diff options
Diffstat (limited to 'src/org/unitConverter/math/ExpressionParser.java')
-rw-r--r-- | src/org/unitConverter/math/ExpressionParser.java | 708 |
1 files changed, 0 insertions, 708 deletions
diff --git a/src/org/unitConverter/math/ExpressionParser.java b/src/org/unitConverter/math/ExpressionParser.java deleted file mode 100644 index b2261ed..0000000 --- a/src/org/unitConverter/math/ExpressionParser.java +++ /dev/null @@ -1,708 +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.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(); - } -} |