summaryrefslogtreecommitdiff
path: root/src/org/unitConverter/math/ExpressionParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/unitConverter/math/ExpressionParser.java')
-rw-r--r--src/org/unitConverter/math/ExpressionParser.java708
1 files changed, 708 insertions, 0 deletions
diff --git a/src/org/unitConverter/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java
new file mode 100644
index 0000000..b2261ed
--- /dev/null
+++ b/src/org/unitConverter/math/ExpressionParser.java
@@ -0,0 +1,708 @@
+/**
+ * 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;
+
+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 <T>
+ * type of object that exists in parsed expressions
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+public final class ExpressionParser<T> {
+ /**
+ * A builder that can create {@code ExpressionParser<T>} instances.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of object that exists in parsed expressions
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public static final class Builder<T> {
+ /**
+ * 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
+ * @since v0.2.0
+ */
+ private final Function<String, T> objectObtainer;
+
+ /**
+ * The function of the space as an operator (like 3 x y)
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private String spaceFunction = null;
+
+ /**
+ * A map mapping operator strings to operator functions, for unary operators.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Map<String, PriorityUnaryOperator<T>> unaryOperators;
+
+ /**
+ * A map mapping operator strings to operator functions, for binary operators.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Map<String, PriorityBinaryOperator<T>> 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
+ * @since v0.2.0
+ */
+ public Builder(final Function<String, T> 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
+ * @since v0.2.0
+ */
+ public Builder<T> addBinaryOperator(final String text, final BinaryOperator<T> 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<T> priorityOperator = new PriorityBinaryOperator<T>(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 function for spaces. You must use the text of an existing binary operator.
+ *
+ * @param operator
+ * text of operator to use
+ * @return this builder
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ public Builder<T> addSpaceFunction(final String operator) {
+ Objects.requireNonNull(operator, "operator must not be null.");
+
+ if (!this.binaryOperators.containsKey(operator))
+ throw new IllegalArgumentException(String.format("Could not find binary operator '%s'", operator));
+
+ this.spaceFunction = operator;
+ return this;
+ }
+
+ /**
+ * Adds a unary operator to the builder.
+ *
+ * @param text
+ * 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
+ * @since v0.2.0
+ */
+ public Builder<T> addUnaryOperator(final String text, final UnaryOperator<T> 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<T> priorityOperator = new PriorityUnaryOperator<T>(priority) {
+ @Override
+ public T apply(final T t) {
+ return operator.apply(t);
+ }
+ };
+ this.unaryOperators.put(text, priorityOperator);
+ return this;
+ }
+
+ /**
+ * @return an {@code ExpressionParser<T>} instance with the properties given to this builder
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public ExpressionParser<T> build() {
+ return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators,
+ this.spaceFunction);
+ }
+ }
+
+ /**
+ * A binary operator with a priority field that determines which operators apply first.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of operand and result
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private static abstract class PriorityBinaryOperator<T>
+ implements BinaryOperator<T>, Comparable<PriorityBinaryOperator<T>> {
+ /**
+ * The operator's priority. Higher-priority operators are applied before lower-priority operators
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private final int priority;
+
+ /**
+ * Creates the {@code PriorityBinaryOperator}.
+ *
+ * @param priority
+ * operator's priority
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public PriorityBinaryOperator(final int priority) {
+ this.priority = priority;
+ }
+
+ /**
+ * Compares this object to another by priority.
+ *
+ * <p>
+ * {@inheritDoc}
+ * </p>
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ @Override
+ public int compareTo(final PriorityBinaryOperator<T> 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
+ * @since v0.2.0
+ */
+ public final int getPriority() {
+ return this.priority;
+ }
+ }
+
+ /**
+ * A unary operator with a priority field that determines which operators apply first.
+ *
+ * @author Adrien Hopkins
+ * @param <T>
+ * type of operand and result
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private static abstract class PriorityUnaryOperator<T>
+ implements UnaryOperator<T>, Comparable<PriorityUnaryOperator<T>> {
+ /**
+ * The operator's priority. Higher-priority operators are applied before lower-priority operators
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private final int priority;
+
+ /**
+ * Creates the {@code PriorityUnaryOperator}.
+ *
+ * @param priority
+ * operator's priority
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ public PriorityUnaryOperator(final int priority) {
+ this.priority = priority;
+ }
+
+ /**
+ * Compares this object to another by priority.
+ *
+ * <p>
+ * {@inheritDoc}
+ * </p>
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ @Override
+ public int compareTo(final PriorityUnaryOperator<T> 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
+ * @since v0.2.0
+ */
+ public final int getPriority() {
+ return this.priority;
+ }
+ }
+
+ /**
+ * The types of tokens that are available.
+ *
+ * @author Adrien Hopkins
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private static enum TokenType {
+ OBJECT, UNARY_OPERATOR, BINARY_OPERATOR;
+ }
+
+ /**
+ * The opening bracket.
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ public static final char OPENING_BRACKET = '(';
+
+ /**
+ * The closing bracket.
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ 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
+ * @since v0.2.0
+ */
+ 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.");
+ }
+
+ /**
+ * 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
+ * @since v0.2.0
+ */
+ private final Function<String, T> objectObtainer;
+
+ /**
+ * A map mapping operator strings to operator functions, for unary operators.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Map<String, PriorityUnaryOperator<T>> unaryOperators;
+
+ /**
+ * A map mapping operator strings to operator functions, for binary operators.
+ *
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private final Map<String, PriorityBinaryOperator<T>> binaryOperators;
+
+ /**
+ * The operator for space, or null if spaces have no function.
+ *
+ * @since 2019-03-22
+ * @since v0.2.0
+ */
+ private final String spaceOperator;
+
+ /**
+ * Creates the {@code ExpressionParser}.
+ *
+ * @param objectObtainer
+ * function to get objects from strings
+ * @param unaryOperators
+ * unary operators available to the parser
+ * @param binaryOperators
+ * binary operators available to the parser
+ * @param spaceOperator
+ * operator used by spaces
+ * @since 2019-03-14
+ * @since v0.2.0
+ */
+ private ExpressionParser(final Function<String, T> objectObtainer,
+ final Map<String, PriorityUnaryOperator<T>> unaryOperators,
+ final Map<String, PriorityBinaryOperator<T>> binaryOperators, final String spaceOperator) {
+ this.objectObtainer = objectObtainer;
+ this.unaryOperators = unaryOperators;
+ this.binaryOperators = binaryOperators;
+ this.spaceOperator = spaceOperator;
+ }
+
+ /**
+ * Converts a given mathematical expression to reverse Polish notation (operators after operands).
+ * <p>
+ * For example,<br>
+ * {@code 2 * (3 + 4)}<br>
+ * becomes<br>
+ * {@code 2 3 4 + *}.
+ *
+ * @param expression
+ * expression
+ * @return expression in RPN
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ 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);
+
+ // check for function
+ if (openingBracketPosition > 0 && partialExpression.charAt(openingBracketPosition - 1) != ' ') {
+ // function like sin(2) or tempF(32)
+ // find the position of the last space
+ int spacePosition = openingBracketPosition;
+ while (spacePosition >= 0 && partialExpression.charAt(spacePosition) != ' ') {
+ spacePosition--;
+ }
+ // then split the function into pre-function and function, using the space position
+ components.addAll(Arrays.asList(partialExpression.substring(0, spacePosition + 1).split(" ")));
+ components.add(partialExpression.substring(spacePosition + 1, closingBracketPosition + 1));
+ partialExpression = partialExpression.substring(closingBracketPosition + 1);
+ } else {
+ // normal brackets like (1 + 2) * (3 / 5)
+ components.addAll(Arrays.asList(partialExpression.substring(0, openingBracketPosition).split(" ")));
+ components.add(this.convertExpressionToReversePolish(
+ partialExpression.substring(openingBracketPosition + 1, closingBracketPosition)));
+ partialExpression = partialExpression.substring(closingBracketPosition + 1);
+ }
+ }
+
+ // add everything else
+ components.addAll(Arrays.asList(partialExpression.split(" ")));
+
+ // remove empty entries
+ while (components.contains("")) {
+ components.remove("");
+ }
+
+ // deal with space multiplication (x y)
+ if (this.spaceOperator != null) {
+ for (int i = 0; i < components.size() - 1; i++) {
+ if (this.getTokenType(components.get(i)) == TokenType.OBJECT
+ && this.getTokenType(components.get(i + 1)) == TokenType.OBJECT) {
+ components.add(++i, this.spaceOperator);
+ }
+ }
+ }
+
+ // turn the expression into reverse Polish
+ while (true) {
+ final int highestPriorityOperatorPosition = this.findHighestPriorityOperatorPosition(components);
+ if (highestPriorityOperatorPosition == -1) {
+ break;
+ }
+
+ // swap components based on what kind of operator there is
+ // 1 + 2 becomes 2 1 +
+ // - 1 becomes 1 -
+ 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;
+ }
+
+ /**
+ * 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
+ * @since v0.2.0
+ */
+ 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;
+ }
+
+ /**
+ * 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
+ * @since v0.2.0
+ */
+ 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
+ * @since v0.2.0
+ */
+ 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
+ * @since v0.2.0
+ */
+ private T parseReversePolishExpression(final String expression) {
+ Objects.requireNonNull(expression, "expression must not be null.");
+
+ final Deque<T> 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<T> 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<T> 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();
+ }
+}