diff --git a/src/main/java/com/sk89q/worldedit/expression/Expression.java b/src/main/java/com/sk89q/worldedit/expression/Expression.java new file mode 100644 index 000000000..28bb0ff80 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/Expression.java @@ -0,0 +1,81 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.sk89q.worldedit.expression.lexer.Lexer; +import com.sk89q.worldedit.expression.lexer.tokens.Token; +import com.sk89q.worldedit.expression.parser.Parser; +import com.sk89q.worldedit.expression.parser.ParserException; +import com.sk89q.worldedit.expression.runtime.Constant; +import com.sk89q.worldedit.expression.runtime.EvaluationException; +import com.sk89q.worldedit.expression.runtime.Invokable; +import com.sk89q.worldedit.expression.runtime.Variable; + +public class Expression { + private final Map variables = new HashMap(); + private final String[] variableNames; + private Invokable root; + + public static Expression compile(String expression, String... variableNames) throws ExpressionException { + return new Expression(expression, variableNames); + } + + private Expression(String expression, String... variableNames) throws ExpressionException { + this(Lexer.tokenize(expression), variableNames); + } + + private Expression(List tokens, String... variableNames) throws ParserException { + this.variableNames = variableNames; + variables.put("e", new Constant(-1, Math.E)); + variables.put("pi", new Constant(-1, Math.PI)); + for (String variableName : variableNames) { + variables.put(variableName, new Variable(0)); + } + + root = Parser.parse(tokens, variables); + } + + public double evaluate(double... values) throws EvaluationException { + for (int i = 0; i < values.length; ++i) { + final String variableName = variableNames[i]; + final Invokable invokable = variables.get(variableName); + if (!(invokable instanceof Variable)) { + throw new EvaluationException(invokable.getPosition(), "Tried to assign constant " + variableName + "."); + } + + ((Variable) invokable).value = values[i]; + } + + return root.invoke(); + } + + public void optimize() throws EvaluationException { + root = root.optimize(); + } + + @Override + public String toString() { + return root.toString(); + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/ExpressionException.java b/src/main/java/com/sk89q/worldedit/expression/ExpressionException.java new file mode 100644 index 000000000..503bfb648 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/ExpressionException.java @@ -0,0 +1,49 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression; + +public class ExpressionException extends Exception { + private static final long serialVersionUID = 1L; + + private final int position; + + public ExpressionException(int position) { + this.position = position; + } + + public ExpressionException(int position, String message, Throwable cause) { + super(message, cause); + this.position = position; + } + + public ExpressionException(int position, String message) { + super(message); + this.position = position; + } + + public ExpressionException(int position, Throwable cause) { + super(cause); + this.position = position; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/Identifiable.java b/src/main/java/com/sk89q/worldedit/expression/Identifiable.java new file mode 100644 index 000000000..d904eea6b --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/Identifiable.java @@ -0,0 +1,46 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression; + +public interface Identifiable { + /** + * Returns a character that helps identify the token, pseudo-token or invokable in question. + * + *
+     * Tokens:
+     * i - IdentifierToken
+     * 0 - NumberToken
+     * o - OperatorToken
+     * \0 - NullToken
+     * CharacterTokens are returned literally
+     *
+     * PseudoTokens:
+     * p - PrefixOperator
+     *
+     * Invokables:
+     * c - Constant
+     * f - Function
+     * v - Variable
+     * 
+ */ + public abstract char id(); + + public int getPosition(); +} diff --git a/src/main/java/com/sk89q/worldedit/expression/lexer/Lexer.java b/src/main/java/com/sk89q/worldedit/expression/lexer/Lexer.java new file mode 100644 index 000000000..e7c2333ed --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/lexer/Lexer.java @@ -0,0 +1,197 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.lexer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.sk89q.worldedit.expression.lexer.tokens.*; + +public class Lexer { + private final String expression; + private int position = 0; + + private Lexer(String expression) { + this.expression = expression; + } + + public static final List tokenize(String expression) throws LexerException { + return new Lexer(expression).tokenize(); + } + + private final DecisionTree operatorTree = new DecisionTree(null, + '-', new DecisionTree("-"), + '+', new DecisionTree("+"), + '*', new DecisionTree("*", + '*', new DecisionTree("^") + ), + '/', new DecisionTree("/"), + '%', new DecisionTree("%"), + '^', new DecisionTree("^"), + '=', new DecisionTree(null, // not implemented + '=', new DecisionTree("==") + ), + '!', new DecisionTree("!", + '=', new DecisionTree("!=") + ), + '<', new DecisionTree("<", + '<', new DecisionTree("<<"), + '=', new DecisionTree("<=") + ), + '>', new DecisionTree(">", + '>', new DecisionTree(">>"), + '=', new DecisionTree(">=") + ), + '&', new DecisionTree(null, // not implemented + '&', new DecisionTree("&&") + ), + '|', new DecisionTree(null, // not implemented + '|', new DecisionTree("||") + ), + '~', new DecisionTree("~", + '=', new DecisionTree("~=") + ) + ); + + private static final Pattern numberPattern = Pattern.compile("^([0-9]*(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)"); + private static final Pattern identifierPattern = Pattern.compile("^([A-Za-z][0-9A-Za-z_]*)"); + + private final List tokenize() throws LexerException { + List tokens = new ArrayList(); + + do { + skipWhitespace(); + if (position >= expression.length()) { + break; + } + + Token token = operatorTree.evaluate(position); + if (token != null) { + tokens.add(token); + continue; + } + + final char ch = peek(); + switch (ch) { + case ',': + case '(': + case ')': + tokens.add(new CharacterToken(position++, ch)); + break; + + default: + final Matcher numberMatcher = numberPattern.matcher(expression.substring(position)); + if (numberMatcher.lookingAt()) { + String numberPart = numberMatcher.group(1); + if (!numberPart.isEmpty()) { + try { + tokens.add(new NumberToken(position, Double.parseDouble(numberPart))); + } + catch (NumberFormatException e) { + throw new LexerException(position, "Number parsing failed", e); + } + + position += numberPart.length(); + continue; + } + } + + final Matcher identifierMatcher = identifierPattern.matcher(expression.substring(position)); + if (identifierMatcher.lookingAt()) { + String identifierPart = identifierMatcher.group(1); + if (!identifierPart.isEmpty()) { + tokens.add(new IdentifierToken(position, identifierPart)); + + position += identifierPart.length(); + continue; + } + } + + throw new LexerException(position, "Unknown character '" + ch + "'"); + } + } + while (position < expression.length()); + + return tokens; + } + + private char peek() { + return expression.charAt(position); + } + + private final void skipWhitespace() { + while (position < expression.length() && Character.isWhitespace(peek())) { + ++position; + } + } + + public class DecisionTree { + private final String tokenName; + private final Map subTrees = new HashMap(); + + private DecisionTree(String tokenName, Object... args) { + this.tokenName = tokenName; + + if (args.length % 2 != 0) { + throw new UnsupportedOperationException("You need to pass an even number of arguments."); + } + + for (int i = 0; i < args.length; i += 2) { + if (!(args[i] instanceof Character)) { + throw new UnsupportedOperationException("Argument #" + i + " expected to be 'Character', not '" + args[i].getClass().getName() + "'."); + } + if (!(args[i + 1] instanceof DecisionTree)) { + throw new UnsupportedOperationException("Argument #" + (i + 1) + " expected to be 'DecisionTree', not '" + args[i + 1].getClass().getName() + "'."); + } + + Character next = (Character) args[i]; + DecisionTree subTree = (DecisionTree) args[i + 1]; + + subTrees.put(next, subTree); + } + } + + private Token evaluate(int startPosition) throws LexerException { + if (position < expression.length()) { + final char next = peek(); + + final DecisionTree subTree = subTrees.get(next); + if (subTree != null) { + ++position; + final Token subTreeResult = subTree.evaluate(startPosition); + if (subTreeResult != null) { + return subTreeResult; + } + --position; + } + } + + if (tokenName == null) { + return null; + } + + return new OperatorToken(startPosition, tokenName); + } + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/lexer/LexerException.java b/src/main/java/com/sk89q/worldedit/expression/lexer/LexerException.java new file mode 100644 index 000000000..dc893088d --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/lexer/LexerException.java @@ -0,0 +1,46 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.lexer; + +import com.sk89q.worldedit.expression.ExpressionException; + +public class LexerException extends ExpressionException { + private static final long serialVersionUID = 1L; + + public LexerException(int position) { + super(position, getPrefix(position)); + } + + public LexerException(int position, String message, Throwable cause) { + super(position, getPrefix(position) + ": " + message, cause); + } + + public LexerException(int position, String message) { + super(position, getPrefix(position) + ": " + message); + } + + public LexerException(int position, Throwable cause) { + super(position, getPrefix(position), cause); + } + + private static String getPrefix(int position) { + return position < 0 ? "Lexer error" : ("Lexer error at " + (position + 1)); + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/CharacterToken.java b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/CharacterToken.java new file mode 100644 index 000000000..36dfe657d --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/CharacterToken.java @@ -0,0 +1,39 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.lexer.tokens; + +public class CharacterToken extends Token { + public final char character; + + public CharacterToken(int position, char character) { + super(position); + this.character = character; + } + + @Override + public char id() { + return character; + } + + @Override + public String toString() { + return "CharacterToken(" + character + ")"; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/IdentifierToken.java b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/IdentifierToken.java new file mode 100644 index 000000000..790824e9e --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/IdentifierToken.java @@ -0,0 +1,39 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.lexer.tokens; + +public class IdentifierToken extends Token { + public final String value; + + public IdentifierToken(int position, String value) { + super(position); + this.value = value; + } + + @Override + public char id() { + return 'i'; + } + + @Override + public String toString() { + return "IdentifierToken(" + value + ")"; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/NumberToken.java b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/NumberToken.java new file mode 100644 index 000000000..1f4858e64 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/NumberToken.java @@ -0,0 +1,39 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.lexer.tokens; + +public class NumberToken extends Token { + public final double value; + + public NumberToken(int position, double value) { + super(position); + this.value = value; + } + + @Override + public char id() { + return '0'; + } + + @Override + public String toString() { + return "NumberToken(" + value + ")"; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/OperatorToken.java b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/OperatorToken.java new file mode 100644 index 000000000..655c2e65d --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/OperatorToken.java @@ -0,0 +1,39 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.lexer.tokens; + +public class OperatorToken extends Token { + public final String operator; + + public OperatorToken(int position, String operator) { + super(position); + this.operator = operator; + } + + @Override + public char id() { + return 'o'; + } + + @Override + public String toString() { + return "OperatorToken(" + operator + ")"; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/Token.java b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/Token.java new file mode 100644 index 000000000..4957a3499 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/Token.java @@ -0,0 +1,35 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.lexer.tokens; + +import com.sk89q.worldedit.expression.Identifiable; + +public abstract class Token implements Identifiable { + private final int position; + + public Token(int position) { + this.position = position; + } + + @Override + public int getPosition() { + return position; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/parser/Parser.java b/src/main/java/com/sk89q/worldedit/expression/parser/Parser.java new file mode 100644 index 000000000..ab76d2490 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/parser/Parser.java @@ -0,0 +1,357 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.parser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.sk89q.worldedit.expression.Identifiable; +import com.sk89q.worldedit.expression.lexer.tokens.IdentifierToken; +import com.sk89q.worldedit.expression.lexer.tokens.NumberToken; +import com.sk89q.worldedit.expression.lexer.tokens.OperatorToken; +import com.sk89q.worldedit.expression.lexer.tokens.Token; +import com.sk89q.worldedit.expression.runtime.Constant; +import com.sk89q.worldedit.expression.runtime.Functions; +import com.sk89q.worldedit.expression.runtime.Invokable; +import com.sk89q.worldedit.expression.runtime.Operators; + +public class Parser { + private final class NullToken extends Token { + private NullToken(int position) { + super(position); + } + + public char id() { + return '\0'; + } + + public String toString() { + return "NullToken"; + } + } + + private final List tokens; + private int position = 0; + private Map variables; + + private Parser(List tokens, Map variables) { + this.tokens = tokens; + this.variables = variables; + } + + public static final Invokable parse(List tokens, Map variables) throws ParserException { + return new Parser(tokens, variables).parse(); + } + + private Invokable parse() throws ParserException { + final Invokable ret = parseInternal(); + if (position < tokens.size()) { + final Token token = peek(); + throw new ParserException(token.getPosition(), "Extra token at the end of the input: " + token); + } + return ret; + } + + private final Invokable parseInternal() throws ParserException { + LinkedList halfProcessed = new LinkedList(); + + // process brackets, numbers, functions, variables and detect prefix operators + boolean expressionStart = true; + loop: while (position < tokens.size()) { + final Token current = peek(); + + switch (current.id()) { + case '0': + halfProcessed.add(new Constant(current.getPosition(), ((NumberToken) current).value)); + ++position; + expressionStart = false; + break; + + case 'i': + final IdentifierToken identifierToken = (IdentifierToken) current; + ++position; + + final Token next = peek(); + if (next.id() == '(') { + halfProcessed.add(parseFunction(identifierToken)); + } + else { + Invokable variable = variables.get(identifierToken.value); + if (variable == null) { + throw new ParserException(current.getPosition(), "Variable '" + identifierToken.value + "' not found"); + } + halfProcessed.add(variable); + } + expressionStart = false; + break; + + case '(': + halfProcessed.add(parseBracket()); + expressionStart = false; + break; + + case ',': + case ')': + break loop; + + case 'o': + if (expressionStart) { + halfProcessed.add(new PrefixOperator((OperatorToken) current)); + } + else { + halfProcessed.add(current); + } + ++position; + expressionStart = true; + break; + + default: + halfProcessed.add(current); + ++position; + expressionStart = false; + break; + } + } + + // process binary operators + return processBinaryOps(halfProcessed, binaryOpMaps.length - 1); + } + + private static final Map[] binaryOpMaps; + + private static final Map unaryOpMap = new HashMap(); + static { + final Object[][][] binaryOps = { + { + { "^", "pow" }, + }, + { + { "*", "mul" }, + { "/", "div" }, + { "%", "mod" }, + }, + { + { "+", "add" }, + { "-", "sub" }, + }, + { + { "<<", "shl" }, + { ">>", "shr" }, + }, + { + { "<", "lth" }, + { ">", "gth" }, + { "<=", "leq" }, + { ">=", "geq" }, + }, + { + { "==", "equ" }, + { "!=", "neq" }, + { "~=", "near" }, + }, + { + { "&&", "and" }, + }, + { + { "||", "or" }, + }, + }; + + @SuppressWarnings("unchecked") + final Map[] tmp = binaryOpMaps = new Map[binaryOps.length]; + for (int i = 0; i < binaryOps.length; ++i) { + final Object[][] a = binaryOps[i]; + switch (a.length) { + case 0: + tmp[i] = Collections.emptyMap(); + break; + + case 1: + final Object[] first = a[0]; + tmp[i] = Collections.singletonMap((String) first[0], (String) first[1]); + break; + + default: + Map m = tmp[i] = new HashMap(); + for (int j = 0; j < a.length; ++j) { + final Object[] element = a[j]; + m.put((String) element[0], (String) element[1]); + } + } + } + + unaryOpMap.put("-", "neg"); + unaryOpMap.put("!", "not"); + unaryOpMap.put("~", "inv"); + } + + private Invokable processBinaryOps(LinkedList input, int level) throws ParserException { + if (level < 0) { + return processUnaryOps(input); + } + + LinkedList lhs = new LinkedList(); + LinkedList rhs = new LinkedList(); + String operator = null; + + for (Iterator it = input.descendingIterator(); it.hasNext();) { + Identifiable identifiable = it.next(); + if (operator == null) { + rhs.addFirst(identifiable); + + if (rhs.isEmpty()) { + continue; + } + + if (!(identifiable instanceof OperatorToken)) { + continue; + } + + operator = binaryOpMaps[level].get(((OperatorToken) identifiable).operator); + if (operator == null) { + continue; + } + + rhs.removeFirst(); + } + else { + lhs.addFirst(identifiable); + } + } + + Invokable rhsInvokable = processBinaryOps(rhs, level - 1); + if (operator == null) return rhsInvokable; + + Invokable lhsInvokable = processBinaryOps(lhs, level); + + try { + return Operators.getOperator(-1, operator, lhsInvokable, rhsInvokable); // TODO: get real position + } + catch (NoSuchMethodException e) { + final Token operatorToken = (Token) input.get(lhs.size()); + throw new ParserException(operatorToken.getPosition(), "Couldn't find operator '" + operator + "'"); + } + } + + private Invokable processUnaryOps(LinkedList input) throws ParserException { + if (input.isEmpty()) { + throw new ParserException(-1, "Expression missing."); + } + + Invokable ret = (Invokable) input.removeLast(); + while (!input.isEmpty()) { + final Identifiable last = input.removeLast(); + if (last instanceof PrefixOperator) { + final String operator = ((PrefixOperator) last).operator; + if (operator.equals("+")) { + continue; + } + + String opName = unaryOpMap.get(operator); + if (opName != null) { + try { + ret = Operators.getOperator(last.getPosition(), opName, ret); + continue; + } + catch (NoSuchMethodException e) { + throw new ParserException(last.getPosition(), "No such prefix operator: " + operator); + } + } + } + if (last instanceof Token) { + throw new ParserException(last.getPosition(), "Extra token found in expression: " + last); + } + else if (last instanceof Invokable) { + throw new ParserException(last.getPosition(), "Extra expression found: " + last); + } + else { + throw new ParserException(last.getPosition(), "Extra element found: " + last); + } + } + return ret; + } + + private Token peek() { + if (position >= tokens.size()) { + return new NullToken(tokens.get(tokens.size() - 1).getPosition() + 1); + } + + return tokens.get(position); + } + + private Identifiable parseFunction(IdentifierToken identifierToken) throws ParserException { + if (peek().id() != '(') { + throw new ParserException(peek().getPosition(), "Unexpected character in parseBracket"); + } + ++position; + + try { + if (peek().id() == ')') { + return Functions.getFunction(identifierToken.getPosition(), identifierToken.value); + } + + List args = new ArrayList(); + + loop: while (true) { + args.add(parseInternal()); + + final Token current = peek(); + ++position; + + switch (current.id()) { + case ',': + continue; + + case ')': + break loop; + + default: + throw new ParserException(current.getPosition(), "Unmatched opening bracket"); + } + } + + return Functions.getFunction(identifierToken.getPosition(), identifierToken.value, args.toArray(new Invokable[args.size()])); + } + catch (NoSuchMethodException e) { + throw new ParserException(identifierToken.getPosition(), "Function not found", e); + } + } + + private final Invokable parseBracket() throws ParserException { + if (peek().id() != '(') { + throw new ParserException(peek().getPosition(), "Unexpected character in parseBracket"); + } + ++position; + + final Invokable ret = parseInternal(); + + if (peek().id() != ')') { + throw new ParserException(peek().getPosition(), "Unmatched opening bracket"); + } + ++position; + + return ret; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/parser/ParserException.java b/src/main/java/com/sk89q/worldedit/expression/parser/ParserException.java new file mode 100644 index 000000000..03b104e05 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/parser/ParserException.java @@ -0,0 +1,46 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.parser; + +import com.sk89q.worldedit.expression.ExpressionException; + +public class ParserException extends ExpressionException { + private static final long serialVersionUID = 1L; + + public ParserException(int position) { + super(position, getPrefix(position)); + } + + public ParserException(int position, String message, Throwable cause) { + super(position, getPrefix(position) + ": " + message, cause); + } + + public ParserException(int position, String message) { + super(position, getPrefix(position) + ": " + message); + } + + public ParserException(int position, Throwable cause) { + super(position, getPrefix(position), cause); + } + + private static String getPrefix(int position) { + return position < 0 ? "Parser error" : ("Parser error at " + (position + 1)); + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/parser/PrefixOperator.java b/src/main/java/com/sk89q/worldedit/expression/parser/PrefixOperator.java new file mode 100644 index 000000000..d710ffe9c --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/parser/PrefixOperator.java @@ -0,0 +1,22 @@ +package com.sk89q.worldedit.expression.parser; + +import com.sk89q.worldedit.expression.lexer.tokens.OperatorToken; + +public class PrefixOperator extends PseudoToken { + final String operator; + + public PrefixOperator(OperatorToken operatorToken) { + super(operatorToken.getPosition()); + operator = operatorToken.operator; + } + + @Override + public char id() { + return 'p'; + } + + @Override + public String toString() { + return "PrefixOperator(" + operator + ")"; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/parser/PseudoToken.java b/src/main/java/com/sk89q/worldedit/expression/parser/PseudoToken.java new file mode 100644 index 000000000..43eab155f --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/parser/PseudoToken.java @@ -0,0 +1,38 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.parser; + +import com.sk89q.worldedit.expression.Identifiable; + +public abstract class PseudoToken implements Identifiable { + private final int position; + + public PseudoToken(int position) { + this.position = position; + } + + @Override + public abstract char id(); + + @Override + public int getPosition() { + return position; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/Constant.java b/src/main/java/com/sk89q/worldedit/expression/runtime/Constant.java new file mode 100644 index 000000000..a1eafc490 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/Constant.java @@ -0,0 +1,44 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.runtime; + +public final class Constant extends Invokable { + private final double value; + + public Constant(int position, double value) { + super(position); + this.value = value; + } + + @Override + public double invoke() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @Override + public char id() { + return 'c'; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/EvaluationException.java b/src/main/java/com/sk89q/worldedit/expression/runtime/EvaluationException.java new file mode 100644 index 000000000..4e016463e --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/EvaluationException.java @@ -0,0 +1,27 @@ +package com.sk89q.worldedit.expression.runtime; + +import com.sk89q.worldedit.expression.ExpressionException; + +public class EvaluationException extends ExpressionException { + private static final long serialVersionUID = 1L; + + public EvaluationException(int position) { + super(position, getPrefix(position)); + } + + public EvaluationException(int position, String message, Throwable cause) { + super(position, getPrefix(position) + ": " + message, cause); + } + + public EvaluationException(int position, String message) { + super(position, getPrefix(position) + ": " + message); + } + + public EvaluationException(int position, Throwable cause) { + super(position, getPrefix(position), cause); + } + + private static String getPrefix(int position) { + return position < 0 ? "Evaluation error" : ("Evaluation error at " + (position + 1)); + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/Function.java b/src/main/java/com/sk89q/worldedit/expression/runtime/Function.java new file mode 100644 index 000000000..41c5584bb --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/Function.java @@ -0,0 +1,94 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.runtime; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class Function extends Invokable { + @Retention(RetentionPolicy.RUNTIME) + public @interface Dynamic { } + + final Method method; + final Invokable[] args; + + Function(int position, Method method, Invokable... args) { + super(position); + this.method = method; + this.args = args; + } + + @Override + public final double invoke() throws EvaluationException { + try { + return (Double) method.invoke(null, (Object[]) args); + } + catch (InvocationTargetException e) { + if (e.getTargetException() instanceof EvaluationException) { + throw (EvaluationException) e.getTargetException(); + } + throw new EvaluationException(-1, "Exception caught while evaluating expression", e.getTargetException()); + } + catch (IllegalAccessException e) { + throw new EvaluationException(-1, "Internal error while evaluating expression", e); + } + } + + @Override + public String toString() { + final StringBuilder ret = new StringBuilder(method.getName()).append('('); + boolean first = true; + for (Object obj : args) { + if (!first) { + ret.append(", "); + } + first = false; + ret.append(obj); + } + return ret.append(')').toString(); + } + + @Override + public char id() { + return 'f'; + } + + @Override + public Invokable optimize() throws EvaluationException { + final Invokable[] optimizedArgs = new Invokable[args.length]; + boolean optimizable = !method.isAnnotationPresent(Dynamic.class); + for (int i = 0; i < args.length; ++i) { + final Invokable optimized = optimizedArgs[i] = args[i].optimize(); + + if (!(optimized instanceof Constant)) { + optimizable = false; + } + } + + if (optimizable) { + return new Constant(getPosition(), invoke()); + } + else { + return new Function(getPosition(), method, optimizedArgs); + } + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/Functions.java b/src/main/java/com/sk89q/worldedit/expression/runtime/Functions.java new file mode 100644 index 000000000..780431c86 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/Functions.java @@ -0,0 +1,129 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.runtime; + +import java.util.Arrays; + +public final class Functions { + public static final Function getFunction(int position, String name, Invokable... args) throws NoSuchMethodException { + final Class[] parameterTypes = (Class[]) new Class[args.length]; + Arrays.fill(parameterTypes, Invokable.class); + return new Function(position, Functions.class.getMethod(name, parameterTypes), args); + } + + + public static final double sin(Invokable x) throws Exception { + return Math.sin(x.invoke()); + } + + public static final double cos(Invokable x) throws Exception { + return Math.cos(x.invoke()); + } + + public static final double tan(Invokable x) throws Exception { + return Math.tan(x.invoke()); + } + + + public static final double asin(Invokable x) throws Exception { + return Math.asin(x.invoke()); + } + + public static final double acos(Invokable x) throws Exception { + return Math.acos(x.invoke()); + } + + public static final double atan(Invokable x) throws Exception { + return Math.atan(x.invoke()); + } + + public static final double atan2(Invokable y, Invokable x) throws Exception { + return Math.atan2(y.invoke(), x.invoke()); + } + + + public static final double sinh(Invokable x) throws Exception { + return Math.sinh(x.invoke()); + } + + public static final double cosh(Invokable x) throws Exception { + return Math.cosh(x.invoke()); + } + + public static final double tanh(Invokable x) throws Exception { + return Math.tanh(x.invoke()); + } + + + public static final double sqrt(Invokable x) throws Exception { + return Math.sqrt(x.invoke()); + } + + public static final double cbrt(Invokable x) throws Exception { + return Math.cbrt(x.invoke()); + } + + + public static final double abs(Invokable x) throws Exception { + return Math.abs(x.invoke()); + } + + public static final double min(Invokable a, Invokable b) throws Exception { + return Math.min(a.invoke(), b.invoke()); + } + + public static final double max(Invokable a, Invokable b) throws Exception { + return Math.max(a.invoke(), b.invoke()); + } + + + public static final double ceil(Invokable x) throws Exception { + return Math.ceil(x.invoke()); + } + + public static final double floor(Invokable x) throws Exception { + return Math.floor(x.invoke()); + } + + public static final double rint(Invokable x) throws Exception { + return Math.rint(x.invoke()); + } + + public static final double round(Invokable x) throws Exception { + return Math.round(x.invoke()); + } + + + public static final double exp(Invokable x) throws Exception { + return Math.exp(x.invoke()); + } + + public static final double ln(Invokable x) throws Exception { + return Math.log(x.invoke()); + } + + public static final double log(Invokable x) throws Exception { + return Math.log(x.invoke()); + } + + public static final double log10(Invokable x) throws Exception { + return Math.log10(x.invoke()); + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/Invokable.java b/src/main/java/com/sk89q/worldedit/expression/runtime/Invokable.java new file mode 100644 index 000000000..a74835a39 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/Invokable.java @@ -0,0 +1,45 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.runtime; + +import com.sk89q.worldedit.expression.Identifiable; + +public abstract class Invokable implements Identifiable { + private final int position; + + public Invokable(int position) { + super(); + this.position = position; + } + + public abstract double invoke() throws EvaluationException; + + @Override + public abstract String toString(); + + public Invokable optimize() throws EvaluationException { + return this; + } + + @Override + public int getPosition() { + return position; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/Operators.java b/src/main/java/com/sk89q/worldedit/expression/runtime/Operators.java new file mode 100644 index 000000000..47d7f888f --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/Operators.java @@ -0,0 +1,136 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.runtime; + +public final class Operators { + public static final Function getOperator(int position, String name, Invokable lhs, Invokable rhs) throws NoSuchMethodException { + return new Function(position, Operators.class.getMethod(name, Invokable.class, Invokable.class), lhs, rhs); + } + + public static final Function getOperator(int position, String name, Invokable argument) throws NoSuchMethodException { + return new Function(position, Operators.class.getMethod(name, Invokable.class), argument); + } + + + public static final double add(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() + rhs.invoke(); + } + + public static final double sub(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() - rhs.invoke(); + } + + public static final double mul(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() * rhs.invoke(); + } + + public static final double div(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() / rhs.invoke(); + } + + public static final double mod(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() % rhs.invoke(); + } + + public static final double pow(Invokable lhs, Invokable rhs) throws EvaluationException { + return Math.pow(lhs.invoke(), rhs.invoke()); + } + + + public static final double neg(Invokable x) throws EvaluationException { + return -x.invoke(); + } + + public static final double not(Invokable x) throws EvaluationException { + return x.invoke() > 0.0 ? 0.0 : 1.0; + } + + public static final double inv(Invokable x) throws EvaluationException { + return ~(long) x.invoke(); + } + + + public static final double lth(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() < rhs.invoke() ? 1.0 : 0.0; + } + + public static final double gth(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() > rhs.invoke() ? 1.0 : 0.0; + } + + public static final double leq(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() <= rhs.invoke() ? 1.0 : 0.0; + } + + public static final double geq(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() >= rhs.invoke() ? 1.0 : 0.0; + } + + + public static final double equ(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() == rhs.invoke() ? 1.0 : 0.0; + } + + public static final double neq(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() != rhs.invoke() ? 1.0 : 0.0; + } + + public static final double near(Invokable lhs, Invokable rhs) throws EvaluationException { + return almostEqual2sComplement(lhs.invoke(), rhs.invoke(), 450359963L) ? 1.0 : 0.0; + //return Math.abs(lhs.invoke() - rhs.invoke()) < 1e-7 ? 1.0 : 0.0; + } + + + public static final double or(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() > 0.0 || rhs.invoke() > 0.0 ? 1.0 : 0.0; + } + + public static final double and(Invokable lhs, Invokable rhs) throws EvaluationException { + return lhs.invoke() > 0.0 && rhs.invoke() > 0.0 ? 1.0 : 0.0; + } + + + public static final double shl(Invokable lhs, Invokable rhs) throws EvaluationException { + return (long) lhs.invoke() << (long) rhs.invoke(); + } + + public static final double shr(Invokable lhs, Invokable rhs) throws EvaluationException { + return (long) lhs.invoke() >> (long) rhs.invoke(); + } + + + // Usable AlmostEqual function, based on http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm + private static boolean almostEqual2sComplement(double A, double B, long maxUlps) { + // Make sure maxUlps is non-negative and small enough that the + // default NAN won't compare as equal to anything. + //assert(maxUlps > 0 && maxUlps < 4 * 1024 * 1024); // this is for floats, not doubles + + long aLong = Double.doubleToRawLongBits(A); + // Make aLong lexicographically ordered as a twos-complement long + if (aLong < 0) aLong = 0x8000000000000000L - aLong; + + long bLong = Double.doubleToRawLongBits(B); + // Make bLong lexicographically ordered as a twos-complement long + if (bLong < 0) bLong = 0x8000000000000000L - bLong; + + long longDiff = Math.abs(aLong - bLong); + return longDiff <= maxUlps; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/Variable.java b/src/main/java/com/sk89q/worldedit/expression/runtime/Variable.java new file mode 100644 index 000000000..fc6ea296d --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/Variable.java @@ -0,0 +1,44 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +package com.sk89q.worldedit.expression.runtime; + +public final class Variable extends Invokable { + public double value; + + public Variable(double value) { + super(-1); + this.value = value; + } + + @Override + public double invoke() { + return value; + } + + @Override + public String toString() { + return "var"; + } + + @Override + public char id() { + return 'v'; + } +} diff --git a/src/test/java/com/sk89q/worldedit/expression/ExpressionTest.java b/src/test/java/com/sk89q/worldedit/expression/ExpressionTest.java new file mode 100644 index 000000000..36a310aaf --- /dev/null +++ b/src/test/java/com/sk89q/worldedit/expression/ExpressionTest.java @@ -0,0 +1,66 @@ +package com.sk89q.worldedit.expression; + +import static org.junit.Assert.*; +import static java.lang.Math.*; + +import org.junit.*; + +import com.sk89q.worldedit.expression.lexer.LexerException; +import com.sk89q.worldedit.expression.parser.ParserException; + +public class ExpressionTest { + @Test + public void testEvaluate() throws Exception { + // check + assertEquals(1-2+3, simpleEval("1-2+3"), 0); + + // check unary ops + assertEquals(2+ +4, simpleEval("2++4"), 0); + assertEquals(2- -4, simpleEval("2--4"), 0); + assertEquals(2*-4, simpleEval("2*-4"), 0); + + // check functions + assertEquals(sin(5), simpleEval("sin(5)"), 0); + assertEquals(atan2(3,4), simpleEval("atan2(3,4)"), 0); + + // check variables + assertEquals(8, Expression.compile("foo+bar", "foo", "bar").evaluate(5, 3), 0); + } + + @Test + public void testErrors() throws ExpressionException { + // test lexer errors + try { + Expression.compile("{"); + fail("Error expected"); + } catch (LexerException e) { + assertEquals("Error position", 0, e.getPosition()); + } + + // test parser errors + try { + Expression.compile("x"); + fail("Error expected"); + } catch (ParserException e) { + assertEquals("Error position", 0, e.getPosition()); + } + try { + Expression.compile("x()"); + fail("Error expected"); + } catch (ParserException e) { + assertEquals("Error position", 0, e.getPosition()); + } + try { + Expression.compile("("); + fail("Error expected"); + } catch (ParserException e) {} + try { + Expression.compile("x("); + fail("Error expected"); + } catch (ParserException e) {} + } + + private double simpleEval(String expression) throws Exception { + return Expression.compile(expression).evaluate(); + } +}