Add support for implicit multiply in equations

This commit is contained in:
ylou
2021-06-13 18:37:38 -07:00
committed by Raoul
parent 4e8dcc4fe4
commit a218a8276b
2 changed files with 58 additions and 28 deletions

View File

@@ -6,6 +6,8 @@ import java.util.NoSuchElementException;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.function.Predicate; import java.util.function.Predicate;
import javax.annotation.Nullable;
/** /**
* Implements evaluation for simple math expressions using the shunting yard * Implements evaluation for simple math expressions using the shunting yard
* algorithm. * algorithm.
@@ -13,47 +15,62 @@ import java.util.function.Predicate;
public class EquationEvaluator { public class EquationEvaluator {
public static double evaluate(String input) { public static double evaluate(String input) {
StringTokenizer tokens = new StringTokenizer(input, "+-*/()", true); final StringTokenizer tokens = new StringTokenizer(input, "+-*/()", true);
Deque<String> operators = new ArrayDeque<>(); final Deque<String> operators = new ArrayDeque<>();
Deque<Double> operands = new ArrayDeque<>(); final Deque<Double> operands = new ArrayDeque<>();
String lastToken = null; String lastToken = null;
while (tokens.hasMoreTokens()) { while (tokens.hasMoreTokens()) {
String rawToken = tokens.nextToken().trim(); String rawToken = tokens.nextToken().trim();
if (rawToken.isEmpty()) if (rawToken.isEmpty())
continue; continue;
if (isUnaryMinus(rawToken, lastToken)) {
boolean isUnaryMinus = rawToken.equals("-") && (lastToken == null rawToken = "u";
|| (!lastToken.equals("u") && isOperator(lastToken)) || lastToken.equals("(")); } else if (isImplicitMultiply(rawToken, lastToken)) {
final String token = isUnaryMinus ? "u" : rawToken; lastToken = processToken(input, "*", operators, operands);
if (isOperator(token)) {
popOperators(input, operators, operands,
top -> (!top.equals("(") && getPrecedence(token) <= getPrecedence(top)));
operators.push(token);
} else if (token.equals("(")) {
operators.push(token);
} else if (token.equals(")")) {
popOperators(input, operators, operands, top -> !top.equals("("));
if (!operators.pop().equals("(")) {
throw new IllegalArgumentException("Unbalanced right parentheses in expression: " + input);
}
} else {
try {
operands.push(Double.parseDouble(token));
} catch (NumberFormatException exception) {
throw new IllegalArgumentException("Could not evaluate expression: " + input);
}
} }
lastToken = token; lastToken = processToken(input, rawToken, operators, operands);
} }
popOperators(input, operators, operands, top -> true); popOperators(input, operators, operands, top -> true);
return operands.pop(); return operands.pop();
} }
private static boolean isUnaryMinus(String rawToken, @Nullable String lastToken) {
return rawToken.equals("-")
&& (lastToken == null || (isOperator(lastToken) && !lastToken.equals("u")) || lastToken.equals("("));
}
private static boolean isImplicitMultiply(String rawToken, @Nullable String lastToken) {
return rawToken.equals("(")
&& (lastToken != null && !isOperator(lastToken) && !lastToken.equals("(") && !lastToken.equals(")"));
}
private static String processToken(String input, String token, Deque<String> operators, Deque<Double> operands) {
if (isOperator(token)) {
popOperators(input, operators, operands,
top -> (!top.equals("(") && getPrecedence(token) <= getPrecedence(top)));
operators.push(token);
} else if (token.equals("(")) {
operators.push(token);
} else if (token.equals(")")) {
popOperators(input, operators, operands, top -> !top.equals("("));
if (!operators.pop().equals("(")) {
throw new IllegalArgumentException("Unbalanced right parentheses in expression: " + input);
}
} else {
try {
operands.push(Double.parseDouble(token));
} catch (NumberFormatException exception) {
throw new IllegalArgumentException("Could not parse double from:" + token);
}
}
return token;
}
private static void popOperators(String input, Deque<String> operators, Deque<Double> operands, private static void popOperators(String input, Deque<String> operators, Deque<Double> operands,
Predicate<String> predicate) { Predicate<String> predicate) {
try { try {
while (!operators.isEmpty() && predicate.test(operators.peek())) { while (!operators.isEmpty() && predicate.test(operators.peek())) {
String op = operators.pop(); final String op = operators.pop();
if (op.equals("u")) { if (op.equals("u")) {
operands.push(-operands.pop()); operands.push(-operands.pop());
continue; continue;
@@ -79,8 +96,8 @@ public class EquationEvaluator {
throw new IllegalArgumentException("Unexpected arithmetic operation " + op); throw new IllegalArgumentException("Unexpected arithmetic operation " + op);
} }
private static boolean isOperator(String op) { private static boolean isOperator(String token) {
switch (op) { switch (token) {
case "+": case "+":
case "-": case "-":
case "*": case "*":

View File

@@ -61,4 +61,17 @@ class EquationEvaluatorTest {
assertFuzzyEquals(1, EquationEvaluator.evaluate("1 - 2 + 3 * 4 / 6")); assertFuzzyEquals(1, EquationEvaluator.evaluate("1 - 2 + 3 * 4 / 6"));
assertFuzzyEquals(4.0 / 3, EquationEvaluator.evaluate("(((1 - 2) + 3) * 4) / 6")); assertFuzzyEquals(4.0 / 3, EquationEvaluator.evaluate("(((1 - 2) + 3) * 4) / 6"));
} }
@Test
void testDivideByZero() {
assertFuzzyEquals(Double.POSITIVE_INFINITY, EquationEvaluator.evaluate("1 / 0"));
assertFuzzyEquals(Double.POSITIVE_INFINITY, EquationEvaluator.evaluate("1 / (1 - 1)"));
}
@Test
void testImplicitMultiply() {
assertFuzzyEquals(-2, EquationEvaluator.evaluate("2(3 + 6) - 5(6 - 2)"));
assertFuzzyEquals(1, EquationEvaluator.evaluate("6 / (2(4 - 1))"));
assertFuzzyEquals(6 * 5 * 4 * 3 * 2, EquationEvaluator.evaluate("6(5(4(3(2(1)))))"));
}
} }