summaryrefslogtreecommitdiff
path: root/src/main/java/sevenUnits/utils/ExpressionParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/sevenUnits/utils/ExpressionParser.java')
-rw-r--r--src/main/java/sevenUnits/utils/ExpressionParser.java308
1 files changed, 231 insertions, 77 deletions
diff --git a/src/main/java/sevenUnits/utils/ExpressionParser.java b/src/main/java/sevenUnits/utils/ExpressionParser.java
index 941c2a4..e248ff0 100644
--- a/src/main/java/sevenUnits/utils/ExpressionParser.java
+++ b/src/main/java/sevenUnits/utils/ExpressionParser.java
@@ -1,5 +1,5 @@
/**
- * Copyright (C) 2019 Adrien Hopkins
+ * Copyright (C) 2019, 2024 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
@@ -24,6 +24,7 @@ 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;
@@ -55,7 +56,7 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
private final Function<String, ? extends T> objectObtainer;
-
+
/**
* The function of the space as an operator (like 3 x y)
*
@@ -63,7 +64,7 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
private String spaceFunction = null;
-
+
/**
* A map mapping operator strings to operator functions, for unary
* operators.
@@ -72,7 +73,7 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
private final Map<String, PriorityUnaryOperator<T>> unaryOperators;
-
+
/**
* A map mapping operator strings to operator functions, for binary
* operators.
@@ -81,7 +82,14 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
private final Map<String, PriorityBinaryOperator<T>> binaryOperators;
-
+
+ /**
+ * A map mapping operator strings to numeric functions.
+ *
+ * @since 2024-03-23
+ */
+ private final Map<String, PriorityBiFunction<T, UncertainDouble, T>> numericOperators;
+
/**
* Creates the {@code Builder}.
*
@@ -96,8 +104,9 @@ public final class ExpressionParser<T> {
"objectObtainer must not be null.");
this.unaryOperators = new HashMap<>();
this.binaryOperators = new HashMap<>();
+ this.numericOperators = new HashMap<>();
}
-
+
/**
* Adds a binary operator to the builder.
*
@@ -115,7 +124,7 @@ public final class ExpressionParser<T> {
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<>(
@@ -124,12 +133,42 @@ public final class ExpressionParser<T> {
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<T> addNumericOperator(final String text,
+ final BiFunction<T, UncertainDouble, 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 PriorityBiFunction<T, UncertainDouble, T> 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.
@@ -141,15 +180,15 @@ public final class ExpressionParser<T> {
*/
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.
*
@@ -167,7 +206,7 @@ public final class ExpressionParser<T> {
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<>(
@@ -180,7 +219,7 @@ public final class ExpressionParser<T> {
this.unaryOperators.put(text, priorityOperator);
return this;
}
-
+
/**
* @return an {@code ExpressionParser<T>} instance with the properties
* given to this builder
@@ -189,10 +228,10 @@ public final class ExpressionParser<T> {
*/
public ExpressionParser<T> build() {
return new ExpressionParser<>(this.objectObtainer, this.unaryOperators,
- this.binaryOperators, this.spaceFunction);
+ this.binaryOperators, this.numericOperators, this.spaceFunction);
}
}
-
+
/**
* A binary operator with a priority field that determines which operators
* apply first.
@@ -212,7 +251,7 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
private final int priority;
-
+
/**
* Creates the {@code PriorityBinaryOperator}.
*
@@ -223,7 +262,7 @@ public final class ExpressionParser<T> {
public PriorityBinaryOperator(final int priority) {
this.priority = priority;
}
-
+
/**
* Compares this object to another by priority.
*
@@ -243,7 +282,68 @@ public final class ExpressionParser<T> {
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 <T> type of operand and result
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ private static abstract class PriorityBiFunction<T, U, R> implements
+ BiFunction<T, U, R>, Comparable<PriorityBiFunction<T, U, R>> {
+ /**
+ * 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.
+ *
+ * <p>
+ * {@inheritDoc}
+ * </p>
+ *
+ * @since 2019-03-17
+ * @since v0.2.0
+ */
+ @Override
+ public int compareTo(final PriorityBiFunction<T, U, R> 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
@@ -253,7 +353,7 @@ public final class ExpressionParser<T> {
return this.priority;
}
}
-
+
/**
* A unary operator with a priority field that determines which operators
* apply first.
@@ -273,7 +373,7 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
private final int priority;
-
+
/**
* Creates the {@code PriorityUnaryOperator}.
*
@@ -284,7 +384,7 @@ public final class ExpressionParser<T> {
public PriorityUnaryOperator(final int priority) {
this.priority = priority;
}
-
+
/**
* Compares this object to another by priority.
*
@@ -304,7 +404,7 @@ public final class ExpressionParser<T> {
else
return 0;
}
-
+
/**
* @return priority
* @since 2019-03-22
@@ -314,7 +414,7 @@ public final class ExpressionParser<T> {
return this.priority;
}
}
-
+
/**
* The types of tokens that are available.
*
@@ -323,9 +423,9 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
private static enum TokenType {
- OBJECT, UNARY_OPERATOR, BINARY_OPERATOR;
+ OBJECT, UNARY_OPERATOR, BINARY_OPERATOR, NUMERIC_OPERATOR;
}
-
+
/**
* The opening bracket.
*
@@ -333,7 +433,7 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
public static final char OPENING_BRACKET = '(';
-
+
/**
* The closing bracket.
*
@@ -341,7 +441,7 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
public static final char CLOSING_BRACKET = ')';
-
+
/**
* Finds the other bracket in a pair of brackets, given the position of one.
*
@@ -355,9 +455,9 @@ public final class ExpressionParser<T> {
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) {
@@ -374,16 +474,16 @@ public final class ExpressionParser<T> {
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) {
@@ -392,10 +492,10 @@ public final class ExpressionParser<T> {
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}.
@@ -404,7 +504,7 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
private final Function<String, ? extends T> objectObtainer;
-
+
/**
* A map mapping operator strings to operator functions, for unary operators.
*
@@ -412,7 +512,7 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
private final Map<String, PriorityUnaryOperator<T>> unaryOperators;
-
+
/**
* A map mapping operator strings to operator functions, for binary
* operators.
@@ -421,7 +521,14 @@ public final class ExpressionParser<T> {
* @since v0.2.0
*/
private final Map<String, PriorityBinaryOperator<T>> binaryOperators;
-
+
+ /**
+ * A map mapping operator strings to numeric functions.
+ *
+ * @since 2024-03-23
+ */
+ private final Map<String, PriorityBiFunction<T, UncertainDouble, T>> numericOperators;
+
/**
* The operator for space, or null if spaces have no function.
*
@@ -429,27 +536,30 @@ public final class ExpressionParser<T> {
* @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
+ * @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<String, ? extends T> objectObtainer,
final Map<String, PriorityUnaryOperator<T>> unaryOperators,
final Map<String, PriorityBinaryOperator<T>> binaryOperators,
+ final Map<String, PriorityBiFunction<T, UncertainDouble, T>> 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).
@@ -468,19 +578,19 @@ public final class ExpressionParser<T> {
*/
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) != ' ') {
@@ -510,15 +620,15 @@ public final class ExpressionParser<T> {
.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++) {
@@ -528,7 +638,7 @@ public final class ExpressionParser<T> {
}
}
}
-
+
// turn the expression into reverse Polish
while (true) {
final int highestPriorityOperatorPosition = this
@@ -536,7 +646,7 @@ public final class ExpressionParser<T> {
if (highestPriorityOperatorPosition == -1) {
break;
}
-
+
// swap components based on what kind of operator there is
// 1 + 2 becomes 2 1 +
// - 1 becomes 1 -
@@ -554,6 +664,7 @@ public final class ExpressionParser<T> {
operand + " " + unaryOperator);
break;
case BINARY_OPERATOR:
+ case NUMERIC_OPERATOR:
if (components.size() < 3)
throw new IllegalArgumentException(
"Invalid expression \"" + expression + "\"");
@@ -570,11 +681,11 @@ public final class ExpressionParser<T> {
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);
}
@@ -583,7 +694,7 @@ public final class ExpressionParser<T> {
}
return expressionRPN;
}
-
+
/**
* Finds the position of the highest-priority operator in a list
*
@@ -601,18 +712,18 @@ public final class ExpressionParser<T> {
// 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;
@@ -622,21 +733,31 @@ public final class ExpressionParser<T> {
final PriorityBinaryOperator<T> 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<T, UncertainDouble, T> 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
*
@@ -648,15 +769,17 @@ public final class ExpressionParser<T> {
*/
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.
*
@@ -670,7 +793,7 @@ public final class ExpressionParser<T> {
return this.parseReversePolishExpression(
this.convertExpressionToReversePolish(expression));
}
-
+
/**
* Parses an expression expressed in reverse Polish notation.
*
@@ -682,55 +805,86 @@ public final class ExpressionParser<T> {
*/
T parseReversePolishExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
-
+
final Deque<T> stack = new ArrayDeque<>();
-
+ final Deque<UncertainDouble> 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<T> binaryOperator = this.binaryOperators
.get(item);
-
+
stack.push(binaryOperator.apply(o1, o2));
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<T, UncertainDouble, T> op = this.numericOperators
+ .get(item);
+ stack.push(op.apply(ot, on));
+ break;
+
case OBJECT:
// just add it to the stack
- stack.push(this.objectObtainer.apply(item));
+ // 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 (Exception e) {
+ try {
+ doubleStack.push(UncertainDouble.fromString(item));
+ } catch (IllegalArgumentException e2) {
+ try {
+ doubleStack.push(
+ UncertainDouble.of(Double.parseDouble(item), 0));
+ } catch (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<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(