summaryrefslogtreecommitdiff
path: root/src/main/java/org/unitConverter/math/ExpressionParser.java
diff options
context:
space:
mode:
authorAdrien Hopkins <ahopk127@my.yorku.ca>2021-06-28 17:16:12 -0500
committerAdrien Hopkins <ahopk127@my.yorku.ca>2021-06-28 17:16:12 -0500
commit78af49e0e5b2ab2eaab87e62c33089c5caa834f8 (patch)
tree14d2a49900d706070882cfe150e08ec1882cdbc2 /src/main/java/org/unitConverter/math/ExpressionParser.java
parenta34d79383061ba53951f3f69a44f142820e82216 (diff)
Renamed project to 7Units
Diffstat (limited to 'src/main/java/org/unitConverter/math/ExpressionParser.java')
-rw-r--r--src/main/java/org/unitConverter/math/ExpressionParser.java735
1 files changed, 0 insertions, 735 deletions
diff --git a/src/main/java/org/unitConverter/math/ExpressionParser.java b/src/main/java/org/unitConverter/math/ExpressionParser.java
deleted file mode 100644
index deee51d..0000000
--- a/src/main/java/org/unitConverter/math/ExpressionParser.java
+++ /dev/null
@@ -1,735 +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, ? extends 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, ? extends 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<>(
- 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<>(
- 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, ? extends 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, ? extends 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();
- }
-}