From 943496888d18b031be19ba8e7348ec188dc8eb6b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 22 Mar 2019 17:00:58 -0400 Subject: Made BaseUnit a subclass of LinearUnit and made an expression parser --- src/org/unitConverter/UnitsDatabase.java | 11 +- .../expressionParser/ExpressionParser.java | 398 ------------- .../expressionParser/package-info.java | 23 - src/org/unitConverter/math/DecimalComparison.java | 107 ++++ src/org/unitConverter/math/ExpressionParser.java | 627 +++++++++++++++++++++ src/org/unitConverter/math/package-info.java | 23 + src/org/unitConverter/unit/BaseUnit.java | 151 +---- src/org/unitConverter/unit/LinearUnit.java | 164 ++++-- src/org/unitConverter/unit/OperatableUnit.java | 169 ------ src/test/java/ExpressionParserTest.java | 50 ++ src/test/java/UnitTest.java | 66 +++ 11 files changed, 1008 insertions(+), 781 deletions(-) delete mode 100644 src/org/unitConverter/expressionParser/ExpressionParser.java delete mode 100644 src/org/unitConverter/expressionParser/package-info.java create mode 100644 src/org/unitConverter/math/DecimalComparison.java create mode 100644 src/org/unitConverter/math/ExpressionParser.java create mode 100644 src/org/unitConverter/math/package-info.java delete mode 100644 src/org/unitConverter/unit/OperatableUnit.java create mode 100644 src/test/java/ExpressionParserTest.java (limited to 'src') 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/expressionParser/ExpressionParser.java b/src/org/unitConverter/expressionParser/ExpressionParser.java deleted file mode 100644 index 804ea87..0000000 --- a/src/org/unitConverter/expressionParser/ExpressionParser.java +++ /dev/null @@ -1,398 +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 . - */ -package org.unitConverter.expressionParser; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.BinaryOperator; -import java.util.function.Function; -import java.util.function.UnaryOperator; - -/** - * An object that can parse expressions with unary or binary operators. - * - * @author Adrien Hopkins - * @param - * type of object that exists in parsed expressions - * @since 2019-03-14 - */ -// TODO: possibly make this class non-final? -public final class ExpressionParser { - /** - * A builder that can create {@code ExpressionParser} instances. - * - * @author Adrien Hopkins - * @param - * type of object that exists in parsed expressions - * @since 2019-03-17 - */ - public static final class Builder { - /** - * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} - * would use {@code Integer::parseInt}. - * - * @since 2019-03-14 - */ - private final Function objectObtainer; - - /** - * A map mapping operator strings to operator functions, for unary operators. - * - * @since 2019-03-14 - */ - private final Map> unaryOperators; - - /** - * A map mapping operator strings to operator functions, for binary operators. - * - * @since 2019-03-14 - */ - private final Map> binaryOperators; - - /** - * Creates the {@code Builder}. - * - * @param objectObtainer - * a function that can turn strings into objects of the type handled by the parser. - * @throws NullPointerException - * if {@code objectObtainer} is null - * @since 2019-03-17 - */ - public Builder(final Function objectObtainer) { - this.objectObtainer = Objects.requireNonNull(objectObtainer, "objectObtainer must not be null."); - this.unaryOperators = new HashMap<>(); - this.binaryOperators = new HashMap<>(); - } - - /** - * Adds a binary operator to the builder. - * - * @param text - * text used to reference the operator, like '+' - * @param operator - * operator to add - * @param priority - * operator's priority, which determines which operators are applied first - * @return this builder - * @throws NullPointerException - * if {@code text} or {@code operator} is null - * @since 2019-03-17 - */ - public Builder addBinaryOperator(final String text, final BinaryOperator operator, final int priority) { - Objects.requireNonNull(text, "text must not be null."); - Objects.requireNonNull(operator, "operator must not be null."); - - // Unfortunately, I cannot use a lambda because the PriorityBinaryOperator requires arguments. - final PriorityBinaryOperator priorityOperator = new PriorityBinaryOperator(priority) { - @Override - public T apply(final T t, final T u) { - return operator.apply(t, u); - } - - }; - this.binaryOperators.put(text, priorityOperator); - return this; - } - - /** - * Adds a unary operator to the builder. - * - * @param text - * text used to reference the operator, like '-' - * @param operator - * operator to add - * @param priority - * operator's priority, which determines which operators are applied first - * @return this builder - * @throws NullPointerException - * if {@code text} or {@code operator} is null - * @since 2019-03-17 - */ - public Builder addUnaryOperator(final String text, final UnaryOperator operator, final int priority) { - Objects.requireNonNull(text, "text must not be null."); - Objects.requireNonNull(operator, "operator must not be null."); - - // Unfortunately, I cannot use a lambda because the PriorityUnaryOperator requires arguments. - final PriorityUnaryOperator priorityOperator = new PriorityUnaryOperator(priority) { - @Override - public T apply(final T t) { - return operator.apply(t); - } - }; - this.unaryOperators.put(text, priorityOperator); - return this; - } - - /** - * @return an {@code ExpressionParser} instance with the properties given to this builder - * @since 2019-03-17 - */ - public ExpressionParser build() { - return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators); - } - } - - /** - * A binary operator with a priority field that determines which operators apply first. - * - * @author Adrien Hopkins - * @param - * type of operand and result - * @since 2019-03-17 - */ - private static abstract class PriorityBinaryOperator - implements BinaryOperator, Comparable> { - /** - * The operator's priority. Higher-priority operators are applied before lower-priority operators - */ - private final int priority; - - /** - * Creates the {@code PriorityBinaryOperator}. - * - * @param priority - * operator's priority - * @since 2019-03-17 - */ - public PriorityBinaryOperator(final int priority) { - this.priority = priority; - } - - /** - * Compares this object to another by priority. - */ - @Override - public int compareTo(final PriorityBinaryOperator o) { - if (this.priority < o.priority) - return -1; - else if (this.priority > o.priority) - return 1; - else - return 0; - } - } - - /** - * A unary operator with a priority field that determines which operators apply first. - * - * @author Adrien Hopkins - * @param - * type of operand and result - * @since 2019-03-17 - */ - private static abstract class PriorityUnaryOperator - implements UnaryOperator, Comparable> { - /** - * The operator's priority. Higher-priority operators are applied before lower-priority operators - */ - private final int priority; - - /** - * Creates the {@code PriorityUnaryOperator}. - * - * @param priority - * operator's priority - * @since 2019-03-17 - */ - public PriorityUnaryOperator(final int priority) { - this.priority = priority; - } - - /** - * Compares this object to another by priority. - */ - @Override - public int compareTo(final PriorityUnaryOperator o) { - if (this.priority < o.priority) - return -1; - else if (this.priority > o.priority) - return 1; - else - return 0; - } - } - - /** - * The types of tokens that are available. - * - * @author Adrien Hopkins - * @since 2019-03-14 - */ - private static enum TokenType { - OBJECT, UNARY_OPERATOR, BINARY_OPERATOR; - } - - /** - * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} would - * use {@code Integer::parseInt}. - * - * @since 2019-03-14 - */ - private final Function objectObtainer; - - /** - * A map mapping operator strings to operator functions, for unary operators. - * - * @since 2019-03-14 - */ - private final Map> unaryOperators; - - /** - * A map mapping operator strings to operator functions, for binary operators. - * - * @since 2019-03-14 - */ - private final Map> binaryOperators; - - /** - * Creates the {@code ExpressionParser}. - * - * @param objectObtainer - * function to get objects from strings - * @param unaryOperators - * operators available to the parser - * @since 2019-03-14 - */ - private ExpressionParser(final Function objectObtainer, - final Map> unaryOperators, - final Map> binaryOperators) { - this.objectObtainer = objectObtainer; - this.unaryOperators = unaryOperators; - this.binaryOperators = binaryOperators; - } - - /** - * Converts a given mathematical expression to reverse Polish notation (operators after operands). - *

- * For example,
- * {@code 2 * (3 + 4)}
- * becomes
- * {@code 2 3 4 + *}. - * - * @param expression - * @return - * @since 2019-03-17 - */ - private String convertExpressionToReversePolish(final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - // TODO method stub org.unitConverter.expressionParser.ExpressionParser.convertExpressionToPolish(expression) - throw new UnsupportedOperationException(); - } - - /** - * Determines whether an inputted string is an object or an operator - * - * @param token - * string to input - * @return type of token it is - * @throws NullPointerException - * if {@code expression} is null - * @since 2019-03-14 - */ - private TokenType getTokenType(final String token) { - Objects.requireNonNull(token, "token must not be null."); - - if (this.unaryOperators.containsKey(token)) - return TokenType.UNARY_OPERATOR; - else if (this.binaryOperators.containsKey(token)) - return TokenType.BINARY_OPERATOR; - else - return TokenType.OBJECT; - } - - /** - * Parses an expression. - * - * @param expression - * expression to parse - * @return result - * @throws NullPointerException - * if {@code expression} is null - * @since 2019-03-14 - */ - public T parseExpression(final String expression) { - return this.parseReversePolishExpression(this.convertExpressionToReversePolish(expression)); - } - - /** - * Parses an expression expressed in reverse Polish notation. - * - * @param expression - * expression to parse - * @return result - * @throws NullPointerException - * if {@code expression} is null - * @since 2019-03-14 - */ - private T parseReversePolishExpression(final String expression) { - Objects.requireNonNull(expression, "expression must not be null."); - - final Deque stack = new ArrayDeque<>(); - - // iterate over every item in the expression, then - for (final String item : expression.split(" ")) { - // choose a path based on what kind of thing was just read - switch (this.getTokenType(item)) { - - case BINARY_OPERATOR: - if (stack.size() < 2) - throw new IllegalStateException(String.format( - "Attempted to call binary operator %s with only %d arguments.", item, stack.size())); - - // get two arguments and operator, then apply! - final T o1 = stack.pop(); - final T o2 = stack.pop(); - final BinaryOperator binaryOperator = this.binaryOperators.get(item); - - stack.push(binaryOperator.apply(o1, o2)); - break; - - case OBJECT: - // just add it to the stack - stack.push(this.objectObtainer.apply(item)); - break; - - case UNARY_OPERATOR: - if (stack.size() < 1) - throw new IllegalStateException(String.format( - "Attempted to call unary operator %s with only %d arguments.", item, stack.size())); - - // get one argument and operator, then apply! - final T o = stack.pop(); - final UnaryOperator unaryOperator = this.unaryOperators.get(item); - - stack.push(unaryOperator.apply(o)); - break; - default: - throw new AssertionError( - String.format("Internal error: Invalid token type %s.", this.getTokenType(item))); - - } - } - - // return answer, or throw an exception if I can't - if (stack.size() > 1) - throw new IllegalStateException("Computation ended up with more than one answer."); - else if (stack.size() == 0) - throw new IllegalStateException("Computation ended up without an answer."); - return stack.pop(); - } -} diff --git a/src/org/unitConverter/expressionParser/package-info.java b/src/org/unitConverter/expressionParser/package-info.java deleted file mode 100644 index 28f0cae..0000000 --- a/src/org/unitConverter/expressionParser/package-info.java +++ /dev/null @@ -1,23 +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 . - */ -/** - * A module that is capable of parsing expressions of things, like mathematical expressions or unit expressions. - * - * @author Adrien Hopkins - * @since 2019-03-14 - */ -package org.unitConverter.expressionParser; \ No newline at end of file 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 . + */ +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/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java new file mode 100644 index 0000000..e06a58b --- /dev/null +++ b/src/org/unitConverter/math/ExpressionParser.java @@ -0,0 +1,627 @@ +/** + * 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 . + */ +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; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * An object that can parse expressions with unary or binary operators. + * + * @author Adrien Hopkins + * @param + * type of object that exists in parsed expressions + * @since 2019-03-14 + */ +// TODO: possibly make this class non-final? +public final class ExpressionParser { + /** + * A builder that can create {@code ExpressionParser} instances. + * + * @author Adrien Hopkins + * @param + * type of object that exists in parsed expressions + * @since 2019-03-17 + */ + public static final class Builder { + /** + * A function that obtains a parseable object from a string. For example, an integer {@code ExpressionParser} + * would use {@code Integer::parseInt}. + * + * @since 2019-03-14 + */ + private final Function objectObtainer; + + /** + * A map mapping operator strings to operator functions, for unary operators. + * + * @since 2019-03-14 + */ + private final Map> unaryOperators; + + /** + * A map mapping operator strings to operator functions, for binary operators. + * + * @since 2019-03-14 + */ + private final Map> binaryOperators; + + /** + * Creates the {@code Builder}. + * + * @param objectObtainer + * a function that can turn strings into objects of the type handled by the parser. + * @throws NullPointerException + * if {@code objectObtainer} is null + * @since 2019-03-17 + */ + public Builder(final Function objectObtainer) { + this.objectObtainer = Objects.requireNonNull(objectObtainer, "objectObtainer must not be null."); + this.unaryOperators = new HashMap<>(); + this.binaryOperators = new HashMap<>(); + } + + /** + * Adds a binary operator to the builder. + * + * @param text + * text used to reference the operator, like '+' + * @param operator + * operator to add + * @param priority + * operator's priority, which determines which operators are applied first + * @return this builder + * @throws NullPointerException + * if {@code text} or {@code operator} is null + * @since 2019-03-17 + */ + public Builder addBinaryOperator(final String text, final BinaryOperator operator, final int priority) { + Objects.requireNonNull(text, "text must not be null."); + Objects.requireNonNull(operator, "operator must not be null."); + + // Unfortunately, I cannot use a lambda because the PriorityBinaryOperator requires arguments. + final PriorityBinaryOperator priorityOperator = new PriorityBinaryOperator(priority) { + @Override + public T apply(final T t, final T u) { + return operator.apply(t, u); + } + + }; + this.binaryOperators.put(text, priorityOperator); + return this; + } + + /** + * Adds a unary operator to the builder. + * + * @param text + * text used to reference the operator, like '-' + * @param operator + * operator to add + * @param priority + * operator's priority, which determines which operators are applied first + * @return this builder + * @throws NullPointerException + * if {@code text} or {@code operator} is null + * @since 2019-03-17 + */ + public Builder addUnaryOperator(final String text, final UnaryOperator operator, final int priority) { + Objects.requireNonNull(text, "text must not be null."); + Objects.requireNonNull(operator, "operator must not be null."); + + // Unfortunately, I cannot use a lambda because the PriorityUnaryOperator requires arguments. + final PriorityUnaryOperator priorityOperator = new PriorityUnaryOperator(priority) { + @Override + public T apply(final T t) { + return operator.apply(t); + } + }; + this.unaryOperators.put(text, priorityOperator); + return this; + } + + /** + * @return an {@code ExpressionParser} instance with the properties given to this builder + * @since 2019-03-17 + */ + public ExpressionParser build() { + return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators); + } + } + + /** + * A binary operator with a priority field that determines which operators apply first. + * + * @author Adrien Hopkins + * @param + * type of operand and result + * @since 2019-03-17 + */ + private static abstract class PriorityBinaryOperator + implements BinaryOperator, Comparable> { + /** + * The operator's priority. Higher-priority operators are applied before lower-priority operators + */ + private final int priority; + + /** + * Creates the {@code PriorityBinaryOperator}. + * + * @param priority + * operator's priority + * @since 2019-03-17 + */ + public PriorityBinaryOperator(final int priority) { + this.priority = priority; + } + + /** + * Compares this object to another by priority. + * + *

+ * {@inheritDoc} + */ + @Override + public int compareTo(final PriorityBinaryOperator o) { + if (this.priority < o.priority) + return -1; + else if (this.priority > o.priority) + return 1; + else + return 0; + } + + /** + * @return priority + * @since 2019-03-22 + */ + public final int getPriority() { + return this.priority; + } + } + + /** + * A unary operator with a priority field that determines which operators apply first. + * + * @author Adrien Hopkins + * @param + * type of operand and result + * @since 2019-03-17 + */ + private static abstract class PriorityUnaryOperator + implements UnaryOperator, Comparable> { + /** + * The operator's priority. Higher-priority operators are applied before lower-priority operators + */ + private final int priority; + + /** + * Creates the {@code PriorityUnaryOperator}. + * + * @param priority + * operator's priority + * @since 2019-03-17 + */ + public PriorityUnaryOperator(final int priority) { + this.priority = priority; + } + + /** + * Compares this object to another by priority. + * + *

+ * {@inheritDoc} + */ + @Override + public int compareTo(final PriorityUnaryOperator o) { + if (this.priority < o.priority) + return -1; + else if (this.priority > o.priority) + return 1; + else + return 0; + } + + /** + * @return priority + * @since 2019-03-22 + */ + public final int getPriority() { + return this.priority; + } + } + + /** + * The types of tokens that are available. + * + * @author Adrien Hopkins + * @since 2019-03-14 + */ + private static enum TokenType { + OBJECT, UNARY_OPERATOR, BINARY_OPERATOR; + } + + /** + * 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 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 void swap(final List 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}. + * + * @since 2019-03-14 + */ + private final Function objectObtainer; + + /** + * A map mapping operator strings to operator functions, for unary operators. + * + * @since 2019-03-14 + */ + private final Map> unaryOperators; + + /** + * A map mapping operator strings to operator functions, for binary operators. + * + * @since 2019-03-14 + */ + private final Map> binaryOperators; + + /** + * Creates the {@code ExpressionParser}. + * + * @param objectObtainer + * function to get objects from strings + * @param unaryOperators + * operators available to the parser + * @since 2019-03-14 + */ + private ExpressionParser(final Function objectObtainer, + final Map> unaryOperators, + final Map> binaryOperators) { + this.objectObtainer = objectObtainer; + this.unaryOperators = unaryOperators; + this.binaryOperators = binaryOperators; + } + + /** + * Converts a given mathematical expression to reverse Polish notation (operators after operands). + *

+ * For example,
+ * {@code 2 * (3 + 4)}
+ * becomes
+ * {@code 2 3 4 + *}. + * + * @param expression + * 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 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) + } + + /** + * 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 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 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 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; + } + + /** + * Determines whether an inputted string is an object or an operator + * + * @param token + * string to input + * @return type of token it is + * @throws NullPointerException + * if {@code expression} is null + * @since 2019-03-14 + */ + private TokenType getTokenType(final String token) { + Objects.requireNonNull(token, "token must not be null."); + + if (this.unaryOperators.containsKey(token)) + return TokenType.UNARY_OPERATOR; + else if (this.binaryOperators.containsKey(token)) + return TokenType.BINARY_OPERATOR; + else + return TokenType.OBJECT; + } + + /** + * Parses an expression. + * + * @param expression + * expression to parse + * @return result + * @throws NullPointerException + * if {@code expression} is null + * @since 2019-03-14 + */ + public T parseExpression(final String expression) { + return this.parseReversePolishExpression(this.convertExpressionToReversePolish(expression)); + } + + /** + * Parses an expression expressed in reverse Polish notation. + * + * @param expression + * expression to parse + * @return result + * @throws NullPointerException + * if {@code expression} is null + * @since 2019-03-14 + */ + private T parseReversePolishExpression(final String expression) { + Objects.requireNonNull(expression, "expression must not be null."); + + final Deque stack = new ArrayDeque<>(); + + // iterate over every item in the expression, then + for (final String item : expression.split(" ")) { + // choose a path based on what kind of thing was just read + switch (this.getTokenType(item)) { + + case BINARY_OPERATOR: + if (stack.size() < 2) + throw new IllegalStateException(String.format( + "Attempted to call binary operator %s with only %d arguments.", item, stack.size())); + + // get two arguments and operator, then apply! + final T o1 = stack.pop(); + final T o2 = stack.pop(); + final BinaryOperator binaryOperator = this.binaryOperators.get(item); + + stack.push(binaryOperator.apply(o1, o2)); + break; + + case OBJECT: + // just add it to the stack + stack.push(this.objectObtainer.apply(item)); + break; + + case UNARY_OPERATOR: + if (stack.size() < 1) + throw new IllegalStateException(String.format( + "Attempted to call unary operator %s with only %d arguments.", item, stack.size())); + + // get one argument and operator, then apply! + final T o = stack.pop(); + final UnaryOperator unaryOperator = this.unaryOperators.get(item); + + stack.push(unaryOperator.apply(o)); + break; + default: + throw new AssertionError( + String.format("Internal error: Invalid token type %s.", this.getTokenType(item))); + + } + } + + // return answer, or throw an exception if I can't + if (stack.size() > 1) + throw new IllegalStateException("Computation ended up with more than one answer."); + else if (stack.size() == 0) + throw new IllegalStateException("Computation ended up without an answer."); + return stack.pop(); + } +} diff --git a/src/org/unitConverter/math/package-info.java b/src/org/unitConverter/math/package-info.java new file mode 100644 index 0000000..65d6b23 --- /dev/null +++ b/src/org/unitConverter/math/package-info.java @@ -0,0 +1,23 @@ +/** + * 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 . + */ +/** + * A module that is capable of parsing expressions of things, like mathematical expressions or unit expressions. + * + * @author Adrien Hopkins + * @since 2019-03-14 + */ +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. + *

+ * 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. + *

* - * @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. + *

+ * 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. + *

* - * @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. + *

+ * 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. + *

* - * @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. + *

+ * 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. + *

+ * + * @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. + *

+ * 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. + *

+ * + * @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. + *

+ * 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. + *

* - * @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 . - */ -package org.unitConverter.unit; - -/** - * A unit that can be added, subtracted, multiplied or divided by another operatable unit, and raised to an integer - * exponent. - *

- * 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. - *

- *

- * 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}. - *

- * - * @author Adrien Hopkins - * @since 2019-03-17 - */ -public interface OperatableUnit extends Unit { - /** - * Returns the quotient of this unit and another. - *

- * 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. - *

- *

- * 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}. - *

- * - * @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. - *

- * Two units can be subtracted if they meet the following conditions: - *

    - *
  • The two units are part of the same UnitSystem.
  • - *
  • The two units have the same {@code dimension}.
  • - *
- * If {@code subtrahend} does not meet these conditions, an {@code IllegalArgumentException} should be thrown. - *

- *

- * 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}. - *

- * - * @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. - *

- * Two units can be added if they meet the following conditions: - *

    - *
  • The two units are part of the same UnitSystem.
  • - *
  • The two units have the same {@code dimension}.
  • - *
- * If {@code addend} does not meet these conditions, an {@code IllegalArgumentException} should be thrown. - *

- *

- * 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}. - *

- * - * @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. - *

- * 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. - *

- *

- * 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}. - *

- * - * @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 . + */ +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 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); + } } -- cgit v1.2.3