/** * Copyright (C) 2019, 2021, 2024, 2025 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 sevenUnits.utils; 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.BiFunction; 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 * @since v0.2.0 */ 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 * @since v0.2.0 */ 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 * @since v0.2.0 */ private final Function 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> unaryOperators; /** * A map mapping operator strings to operator functions, for binary * operators. * * @since 2019-03-14 * @since v0.2.0 */ private final Map> binaryOperators; /** * A map mapping operator strings to numeric functions. * * @since 2024-03-23 * @since v0.5.0 */ private final Map> numericOperators; /** * 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 objectObtainer) { this.objectObtainer = Objects.requireNonNull(objectObtainer, "objectObtainer must not be null."); this.unaryOperators = new HashMap<>(); this.binaryOperators = new HashMap<>(); this.numericOperators = 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 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 two-argument operator where the second operator is a number. * This is used for operations like vector scaling and exponentation. * * @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 */ public Builder addNumericOperator(final String text, final BiFunction 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 PriorityBiFunction priorityOperator = new PriorityBiFunction<>( priority) { @Override public T apply(final T t, final UncertainDouble u) { return operator.apply(t, u); } }; this.numericOperators.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 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 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 * @since v0.2.0 */ public ExpressionParser build() { return new ExpressionParser<>(this.objectObtainer, this.unaryOperators, this.binaryOperators, this.numericOperators, this.spaceFunction); } } /** * 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 * @since v0.2.0 */ private static abstract class PriorityBiFunction implements BiFunction, Comparable> { /** * 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 PriorityBiFunction(final int priority) { this.priority = priority; } /** * Compares this object to another by priority. * *

* {@inheritDoc} *

* * @since 2019-03-17 * @since v0.2.0 */ @Override public int compareTo(final PriorityBiFunction 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 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 * @since v0.2.0 */ private static abstract class PriorityBinaryOperator implements BinaryOperator, Comparable> { /** * 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. * *

* {@inheritDoc} *

* * @since 2019-03-17 * @since v0.2.0 */ @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 * @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 type of operand and result * @since 2019-03-17 * @since v0.2.0 */ private static abstract class PriorityUnaryOperator implements UnaryOperator, Comparable> { /** * 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. * *

* {@inheritDoc} *

* * @since 2019-03-17 * @since v0.2.0 */ @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 * @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, NUMERIC_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 objectObtainer; /** * A map mapping operator strings to operator functions, for unary operators. * * @since 2019-03-14 * @since v0.2.0 */ private final Map> unaryOperators; /** * A map mapping operator strings to operator functions, for binary * operators. * * @since 2019-03-14 * @since v0.2.0 */ private final Map> binaryOperators; /** * A map mapping operator strings to numeric functions. * * @since 2024-03-23 * @since v0.5.0 */ private final Map> numericOperators; /** * 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 numericOperators numeric operators available to the parser * @param spaceOperator operator used by spaces * @since 2019-03-14 * @since v0.2.0 */ private ExpressionParser(final Function objectObtainer, final Map> unaryOperators, final Map> binaryOperators, final Map> numericOperators, final String spaceOperator) { this.objectObtainer = objectObtainer; this.unaryOperators = unaryOperators; this.binaryOperators = binaryOperators; this.numericOperators = numericOperators; this.spaceOperator = spaceOperator; } /** * 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 * @throws IllegalArgumentException if expression is invalid (e.g. * "{@code 3 *}") * @since 2019-03-17 * @since v0.2.0 */ 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); // 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: if (components.size() < 2) throw new IllegalArgumentException( "Invalid expression \"" + expression + "\""); final String unaryOperator = components .remove(highestPriorityOperatorPosition); final String operand = components .remove(highestPriorityOperatorPosition); components.add(highestPriorityOperatorPosition, operand + " " + unaryOperator); break; case BINARY_OPERATOR: case NUMERIC_OPERATOR: if (components.size() < 3) throw new IllegalArgumentException( "Invalid expression \"" + expression + "\""); final String binaryOperator = components .remove(highestPriorityOperatorPosition); final String operand1 = components .remove(highestPriorityOperatorPosition - 1); final String operand2 = components .remove(highestPriorityOperatorPosition - 1); components.add(highestPriorityOperatorPosition - 1, operand1 + " " + operand2 + " " + 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 if (components.size() != 1) throw new IllegalArgumentException( "Invalid expression \"" + expression + "\"."); return components.get(0).replaceAll(" +", " ").trim(); } /** * 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 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; case NUMERIC_OPERATOR: final PriorityBiFunction numericOperator = this.numericOperators .get(components.get(i)); final int numericPriority = numericOperator.getPriority(); if (numericPriority > maxPriority) { maxPriority = numericPriority; 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 if (this.numericOperators.containsKey(token)) return TokenType.NUMERIC_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 */ T parseReversePolishExpression(final String expression) { Objects.requireNonNull(expression, "expression must not be null."); final Deque stack = new ArrayDeque<>(); final Deque doubleStack = 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(o2, o1)); break; case NUMERIC_OPERATOR: if (stack.size() < 1 || doubleStack.size() < 1) throw new IllegalStateException(String.format( "Attempted to call binary operator %s with insufficient arguments.", item)); final T ot = stack.pop(); final UncertainDouble on = doubleStack.pop(); final BiFunction op = this.numericOperators .get(item); stack.push(op.apply(ot, on)); break; case OBJECT: // just add it to the stack // these try-catch statements are necessary // to make the code as generalizable as possible // also they're required for number formatting code because // that's the only way to tell if an expression is a number or not. try { stack.push(this.objectObtainer.apply(item)); } catch (final Exception e) { try { doubleStack.push(UncertainDouble.fromString(item)); } catch (final IllegalArgumentException e2) { try { doubleStack.push( UncertainDouble.of(Double.parseDouble(item), 0)); } catch (final NumberFormatException e3) { throw e; } } } 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(); } }