> {
/**
* The operator's priority. Higher-priority operators are applied before lower-priority operators
*/
private final int priority;
/**
* Creates the {@code PriorityBinaryOperator}.
*
* @param priority
* operator's priority
* @since 2019-03-17
*/
public PriorityBinaryOperator(final int priority) {
this.priority = priority;
}
/**
* Compares this object to another by priority.
*
*
* {@inheritDoc}
*/
@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
*/
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
*/
private static abstract class PriorityUnaryOperator
implements UnaryOperator, Comparable> {
/**
* The operator's priority. Higher-priority operators are applied before lower-priority operators
*/
private final int priority;
/**
* Creates the {@code PriorityUnaryOperator}.
*
* @param priority
* operator's priority
* @since 2019-03-17
*/
public PriorityUnaryOperator(final int priority) {
this.priority = priority;
}
/**
* Compares this object to another by priority.
*
*
* {@inheritDoc}
*/
@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
*/
public final int getPriority() {
return this.priority;
}
}
/**
* The types of tokens that are available.
*
* @author Adrien Hopkins
* @since 2019-03-14
*/
private static enum TokenType {
OBJECT, UNARY_OPERATOR, BINARY_OPERATOR;
}
/**
* The opening bracket.
*
* @since 2019-03-22
*/
public static final char OPENING_BRACKET = '(';
/**
* The closing bracket.
*
* @since 2019-03-22
*/
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
*/
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
*/
private final Function objectObtainer;
/**
* A map mapping operator strings to operator functions, for unary operators.
*
* @since 2019-03-14
*/
private final Map> unaryOperators;
/**
* A map mapping operator strings to operator functions, for binary operators.
*
* @since 2019-03-14
*/
private final Map> binaryOperators;
/**
* The operator for space, or null if spaces have no function.
*
* @since 2019-03-22
*/
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
*/
private ExpressionParser(final Function objectObtainer,
final Map> unaryOperators,
final Map> 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).
*
* For example,
* {@code 2 * (3 + 4)}
* becomes
* {@code 2 3 4 + *}.
*
* @param expression
* expression
* @return expression in RPN
* @since 2019-03-17
*/
private 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:
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;
// TODO document org.unitConverter.expressionParser.ExpressionParser.convertExpressionToPolish(expression)
}
/**
* 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
*/
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;
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
*/
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
*/
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
*/
private T parseReversePolishExpression(final String expression) {
Objects.requireNonNull(expression, "expression must not be null.");
final Deque 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 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 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();
}
}