diff --git a/src/main/java/net/objecthunter/exp4j/ExpressionBuilder.java b/src/main/java/net/objecthunter/exp4j/ExpressionBuilder.java index 944e577b..4630d0ce 100644 --- a/src/main/java/net/objecthunter/exp4j/ExpressionBuilder.java +++ b/src/main/java/net/objecthunter/exp4j/ExpressionBuilder.java @@ -22,6 +22,7 @@ import net.objecthunter.exp4j.function.Functions; import net.objecthunter.exp4j.operator.Operator; import net.objecthunter.exp4j.shuntingyard.ShuntingYard; +import net.objecthunter.exp4j.tokenizer.Tokenizer; /** * Factory class for {@link Expression} instances. This class is the main API entrypoint. Users should create new @@ -111,10 +112,15 @@ public ExpressionBuilder operator(Operator operator) { return this; } + private Tokenizer operatorChecker = null; + private void checkOperatorSymbol(Operator op) { + if (operatorChecker == null) { + operatorChecker = createTokenizer(); + } String name = op.getSymbol(); for (char ch : name.toCharArray()) { - if (!Operator.isAllowedOperatorChar(ch)) { + if (! operatorChecker.isAllowedOperatorChar(ch)) { throw new IllegalArgumentException("The operator symbol '" + name + "' is invalid"); } } @@ -163,8 +169,12 @@ public Expression build() { throw new IllegalArgumentException("A variable can not have the same name as a function [" + var + "]"); } } - return new Expression(ShuntingYard.convertToRPN(this.expression, this.userFunctions, this.userOperators, this.variableNames), + return new Expression(ShuntingYard.convertToRPN(this.expression, this.userFunctions, this.userOperators, this.variableNames, createTokenizer()), this.userFunctions.keySet()); } + + protected Tokenizer createTokenizer() { + return new Tokenizer(expression, userFunctions, userOperators, variableNames); + } } diff --git a/src/main/java/net/objecthunter/exp4j/operator/Operator.java b/src/main/java/net/objecthunter/exp4j/operator/Operator.java index f074ea1f..05ed8449 100644 --- a/src/main/java/net/objecthunter/exp4j/operator/Operator.java +++ b/src/main/java/net/objecthunter/exp4j/operator/Operator.java @@ -52,11 +52,6 @@ public abstract class Operator { */ public static final int PRECEDENCE_UNARY_PLUS = PRECEDENCE_UNARY_MINUS; - /** - * The set of allowed operator chars - */ - public static final char[] ALLOWED_OPERATOR_CHARS = { '+', '-', '*', '/', '%', '^', '!', '#','§', '$', '&', ';', ':', '~', '<', '>', '|', '='}; - protected final int numOperands; protected final boolean leftAssociative; protected final String symbol; @@ -78,20 +73,6 @@ public Operator(String symbol, int numberOfOperands, boolean leftAssociative, this.precedence = precedence; } - /** - * Check if a character is an allowed operator char - * @param ch the char to check - * @return true if the char is allowed an an operator symbol, false otherwise - */ - public static boolean isAllowedOperatorChar(char ch) { - for (char allowed: ALLOWED_OPERATOR_CHARS) { - if (ch == allowed) { - return true; - } - } - return false; - } - /** * Check if the operator is left associative * @return true os the operator is left associative, false otherwise diff --git a/src/main/java/net/objecthunter/exp4j/shuntingyard/ShuntingYard.java b/src/main/java/net/objecthunter/exp4j/shuntingyard/ShuntingYard.java index b9754819..fe09f2c2 100644 --- a/src/main/java/net/objecthunter/exp4j/shuntingyard/ShuntingYard.java +++ b/src/main/java/net/objecthunter/exp4j/shuntingyard/ShuntingYard.java @@ -37,11 +37,10 @@ public class ShuntingYard { * @return a {@link net.objecthunter.exp4j.tokenizer.Token} array containing the result */ public static Token[] convertToRPN(final String expression, final Map userFunctions, - final Map userOperators, final Set variableNames){ + final Map userOperators, final Set variableNames, Tokenizer tokenizer){ final Stack stack = new Stack(); final List output = new ArrayList(); - final Tokenizer tokenizer = new Tokenizer(expression, userFunctions, userOperators, variableNames); while (tokenizer.hasNext()) { Token token = tokenizer.nextToken(); switch (token.getType()) { diff --git a/src/main/java/net/objecthunter/exp4j/tokenizer/Tokenizer.java b/src/main/java/net/objecthunter/exp4j/tokenizer/Tokenizer.java index 011b242e..00ef91ad 100644 --- a/src/main/java/net/objecthunter/exp4j/tokenizer/Tokenizer.java +++ b/src/main/java/net/objecthunter/exp4j/tokenizer/Tokenizer.java @@ -15,6 +15,8 @@ */ package net.objecthunter.exp4j.tokenizer; +import java.util.ArrayList; +import java.util.Collection; import java.util.Map; import java.util.Set; @@ -86,9 +88,9 @@ public Token nextToken(){ return parseParentheses(true); } else if (isCloseParentheses(ch)) { return parseParentheses(false); - } else if (Operator.isAllowedOperatorChar(ch)) { + } else if (isAllowedOperatorChar(ch)) { return parseOperatorToken(ch); - } else if (isAlphabetic(ch) || ch == '_') { + } else if (isVariableOrFunctionStartChar(ch)) { // parse the name which can be a setVariable or a function if (lastToken != null && (lastToken.getType() != Token.TOKEN_OPERATOR @@ -105,7 +107,20 @@ public Token nextToken(){ throw new IllegalArgumentException("Unable to parse char '" + ch + "' (Code:" + (int) ch + ") at [" + pos + "]"); } - private Token parseArgumentSeparatorToken(char ch) { + protected boolean isVariableOrFunctionStartChar(char ch) { + return isAlphabetic(ch) || ch == '_'; + } + + /** + * The set of allowed operator chars + */ + public static final String DEFAULT_ALLOWED_OPERATOR_CHARS = "+-*/%^!#§$&;:~<>|="; + + public boolean isAllowedOperatorChar(char ch) { + return DEFAULT_ALLOWED_OPERATOR_CHARS.indexOf(ch) >= 0; + } + + private Token parseArgumentSeparatorToken(char ch) { this.pos++; this.lastToken = new ArgumentSeparatorToken(); return lastToken; @@ -133,6 +148,16 @@ private boolean isCloseParentheses(char ch) { return ch == ')' || ch == '}' || ch == ']'; } + private Collection undefinedVariables = null; + + public void allowUndefinedVariables(boolean allow) { + this.undefinedVariables = (allow ? new ArrayList() : null); + } + + public String[] getUndefinedVariables() { + return (undefinedVariables != null ? undefinedVariables.toArray(new String[undefinedVariables.size()]) : null); + } + private Token parseFunctionOrVariable() { final int offset = this.pos; int lastValidLen = 1; @@ -154,6 +179,12 @@ private Token parseFunctionOrVariable() { if (f != null) { lastValidLen = len; lastValidToken = new FunctionToken(f); + } else if (undefinedVariables != null) { + if (! undefinedVariables.contains(name)) { + undefinedVariables.add(name); + } + lastValidLen = len; + lastValidToken = new VariableToken(name); } } len++; @@ -184,7 +215,7 @@ private Token parseOperatorToken(char firstChar) { Operator lastValid = null; symbol.append(firstChar); - while (!isEndOfExpression(offset + len) && Operator.isAllowedOperatorChar(expression[offset + len])) { + while (!isEndOfExpression(offset + len) && isAllowedOperatorChar(expression[offset + len])) { symbol.append(expression[offset + len++]); } diff --git a/src/test/java/net/objecthunter/exp4j/shuntingyard/ShuntingYardTest.java b/src/test/java/net/objecthunter/exp4j/shuntingyard/ShuntingYardTest.java index 1a226ec6..82cb490b 100644 --- a/src/test/java/net/objecthunter/exp4j/shuntingyard/ShuntingYardTest.java +++ b/src/test/java/net/objecthunter/exp4j/shuntingyard/ShuntingYardTest.java @@ -21,9 +21,12 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Set; +import net.objecthunter.exp4j.function.Function; import net.objecthunter.exp4j.operator.Operator; import net.objecthunter.exp4j.tokenizer.Token; +import net.objecthunter.exp4j.tokenizer.Tokenizer; import org.junit.Test; @@ -32,16 +35,20 @@ public class ShuntingYardTest { @Test public void testShuntingYard1() throws Exception { String expression = "2+3"; - Token[] tokens = ShuntingYard.convertToRPN(expression, null, null, null); + Token[] tokens = convertToRPN(expression, null, null, null); assertNumberToken(tokens[0], 2d); assertNumberToken(tokens[1], 3d); assertOperatorToken(tokens[2], "+", 2, Operator.PRECEDENCE_ADDITION); } + protected Token[] convertToRPN(String expression, Map userFunctions, Map userOperators, Set variableNames) { + return ShuntingYard.convertToRPN(expression, userFunctions, userOperators, variableNames, new Tokenizer(expression, userFunctions, userOperators, variableNames)); + } + @Test public void testShuntingYard2() throws Exception { String expression = "3*x"; - Token[] tokens = ShuntingYard.convertToRPN(expression, null, null, new HashSet(Arrays.asList("x"))); + Token[] tokens = convertToRPN(expression, null, null, new HashSet(Arrays.asList("x"))); assertNumberToken(tokens[0], 3d); assertVariableToken(tokens[1], "x"); assertOperatorToken(tokens[2], "*", 2, Operator.PRECEDENCE_MULTIPLICATION); @@ -50,7 +57,7 @@ public void testShuntingYard2() throws Exception { @Test public void testShuntingYard3() throws Exception { String expression = "-3"; - Token[] tokens = ShuntingYard.convertToRPN(expression, null, null, null); + Token[] tokens = convertToRPN(expression, null, null, null); assertNumberToken(tokens[0], 3d); assertOperatorToken(tokens[1], "-", 1, Operator.PRECEDENCE_UNARY_MINUS); } @@ -58,7 +65,7 @@ public void testShuntingYard3() throws Exception { @Test public void testShuntingYard4() throws Exception { String expression = "-2^2"; - Token[] tokens = ShuntingYard.convertToRPN(expression, null, null, null); + Token[] tokens = convertToRPN(expression, null, null, null); assertNumberToken(tokens[0], 2d); assertNumberToken(tokens[1], 2d); assertOperatorToken(tokens[2], "^", 2, Operator.PRECEDENCE_POWER); @@ -68,7 +75,7 @@ public void testShuntingYard4() throws Exception { @Test public void testShuntingYard5() throws Exception { String expression = "2^-2"; - Token[] tokens = ShuntingYard.convertToRPN(expression, null, null, null); + Token[] tokens = convertToRPN(expression, null, null, null); assertNumberToken(tokens[0], 2d); assertNumberToken(tokens[1], 2d); assertOperatorToken(tokens[2], "-", 1, Operator.PRECEDENCE_UNARY_MINUS); @@ -77,7 +84,7 @@ public void testShuntingYard5() throws Exception { @Test public void testShuntingYard6() throws Exception { String expression = "2^---+2"; - Token[] tokens = ShuntingYard.convertToRPN(expression, null, null, null); + Token[] tokens = convertToRPN(expression, null, null, null); assertNumberToken(tokens[0], 2d); assertNumberToken(tokens[1], 2d); assertOperatorToken(tokens[2], "+", 1, Operator.PRECEDENCE_UNARY_PLUS); @@ -109,7 +116,7 @@ public double apply(double... args) { }; Map userOperators = new HashMap(); userOperators.put("!", factorial); - Token[] tokens = ShuntingYard.convertToRPN(expression, null, userOperators, null); + Token[] tokens = convertToRPN(expression, null, userOperators, null); assertNumberToken(tokens[0], 2d); assertNumberToken(tokens[1], 2d); assertOperatorToken(tokens[2], "!", 1, Operator.PRECEDENCE_POWER + 1); @@ -120,7 +127,7 @@ public double apply(double... args) { @Test public void testShuntingYard8() throws Exception { String expression = "-3^2"; - Token[] tokens = ShuntingYard.convertToRPN(expression, null, null, null); + Token[] tokens = convertToRPN(expression, null, null, null); assertNumberToken(tokens[0], 3d); assertNumberToken(tokens[1], 2d); assertOperatorToken(tokens[2], "^", 2, Operator.PRECEDENCE_POWER); @@ -140,7 +147,7 @@ public double apply(final double... args) { }; Map userOperators = new HashMap(); userOperators.put("$", reciprocal); - Token[] tokens = ShuntingYard.convertToRPN("1$", null, userOperators, null); + Token[] tokens = convertToRPN("1$", null, userOperators, null); assertNumberToken(tokens[0], 1d); assertOperatorToken(tokens[1], "$", 1, Operator.PRECEDENCE_DIVISION); } diff --git a/src/test/java/net/objecthunter/exp4j/tokenizer/TokenizerTest.java b/src/test/java/net/objecthunter/exp4j/tokenizer/TokenizerTest.java index e8ea8b4e..74f5aa1a 100644 --- a/src/test/java/net/objecthunter/exp4j/tokenizer/TokenizerTest.java +++ b/src/test/java/net/objecthunter/exp4j/tokenizer/TokenizerTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.*; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -551,4 +552,51 @@ public void testTokenization22() throws Exception { assertFalse(tokenizer.hasNext()); } + + @Test + public void testAllowUndefinedVariablesFalse() { + final Tokenizer tokenizer = new Tokenizer("a * b", null, null, Collections.singleton("a")); + tokenizer.allowUndefinedVariables(false); + + assertTrue(tokenizer.hasNext()); + Token tok1 = tokenizer.nextToken(); + assertTrue(tok1 instanceof VariableToken); + assertEquals("a", ((VariableToken) tok1).getName()); + String[] undefinedVariables = tokenizer.getUndefinedVariables(); + assertFalse(undefinedVariables != null && undefinedVariables.length > 0); + + assertTrue(tokenizer.hasNext()); + assertOperatorToken(tokenizer.nextToken(), "*", 2, Operator.PRECEDENCE_MULTIPLICATION); + + assertTrue(tokenizer.hasNext()); + try { + tokenizer.nextToken(); + fail(); + } catch (Exception e) { + assertTrue(e instanceof IllegalArgumentException); + } + } + + @Test + public void testAllowUndefinedVariablesTrue() { + final Tokenizer tokenizer = new Tokenizer("a * b", null, null, Collections.singleton("a")); + tokenizer.allowUndefinedVariables(true); + + assertTrue(tokenizer.hasNext()); + Token tok1 = tokenizer.nextToken(); + assertTrue(tok1 instanceof VariableToken); + assertEquals("a", ((VariableToken) tok1).getName()); + assertEquals(0, tokenizer.getUndefinedVariables().length); + + assertTrue(tokenizer.hasNext()); + assertOperatorToken(tokenizer.nextToken(), "*", 2, Operator.PRECEDENCE_MULTIPLICATION); + + assertTrue(tokenizer.hasNext()); + Token tok3 = tokenizer.nextToken(); + assertTrue(tok3 instanceof VariableToken); + assertEquals("b", ((VariableToken) tok3).getName()); + String[] undefinedVariables = tokenizer.getUndefinedVariables(); + assertEquals(1, undefinedVariables.length); + assertEquals("b", undefinedVariables[0]); + } }