Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use String parameters in custom functions #70

Closed
irkasper opened this issue May 23, 2016 · 7 comments
Closed

use String parameters in custom functions #70

irkasper opened this issue May 23, 2016 · 7 comments

Comments

@irkasper
Copy link

Hi dear
I want to evaluate a function like this: myFunction(23A12B,H341B1), but the parser didn't recongize the whole 23A12B as a parameter!
how do i can do this?

@uklimaschewski
Copy link
Collaborator

Is 23A12B meant to be a variable? Variable names have to start with a letter or underscore.

@irkasper
Copy link
Author

‍Hi Guy

Thanks for your reply

Is 23A12B meant to be a variable? No! this is a product number

i have a custom function called QTY(productNumber), it returns qty of
this products, in my project, productNumbers have alphabetic chars in
it, but your functions only accept bigDecimals as parameter, how i can
pass parameters like this?

On ۱۶/۰۵/۲۴ 11:17, Udo Klimaschewski wrote:

Is 23A12B meant to be a variable? Variable names have to start with a
letter or underscore.


You are receiving this because you authored the thread.
Reply to this email directly or view it on GitHub
#70 (comment)

@uklimaschewski
Copy link
Collaborator

I am sorry, but this is not possible. The parameters to functions are numbers, variable, constants or expressions. Strings are not supported.

@irkasper
Copy link
Author

‍oh my god!
can you suggest me any way?!
it's urgent for me!

On سه‌شنبه, مه 24, 2016 at 11:53 , Udo Klimaschewski
notifications@github.com wrote:

I am sorry, but this is not possible. The parameters to functions are
numbers, variable, constants or expressions. Strings are not
supported.


You are receiving this because you authored the thread.
Reply to this email directly or view it on GitHub

@uklimaschewski
Copy link
Collaborator

There might be a possible solution, for this the LazyNumber interface has to be extended:

    interface LazyNumber {
        BigDecimal eval();
        String getString();
    }

The getString() method should return the expression string, or the token, e.g. in the eval() method:

    } else {
        stack.push(new LazyNumber() {
            public BigDecimal eval() {
                return new BigDecimal(token, mc);
            }
            public String getString() {
                return token;
            }
        });
    }

then you could define a function and extract the parameter string, but you have to make sure the parameter starts with a letter:

        Expression e = new Expression("QTY(product_id)").with("product_id", "P_23A12B");
        e.addLazyFunction(e.new LazyFunction("QTY", 1) {
            @Override
            public LazyNumber lazyEval(List<LazyNumber> lazyParams) {
                String s = lazyParams.get(0).getString();
                // String s now holds "P_23A12B"
                return lazyParams.get(0);
            }
        });
        BigDecimal result = e.eval();

If you put a bit effort into the tokenizer, it should also be possible to support string literals as tokens.
But this is all more or less a hack, not a real solution, as this library only supports BigDecimal under the hood.

@irkasper
Copy link
Author

Now, my problem is solved!
Thanks you dear guy

here my changed code:

‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍public class Expression {

/**
 * Definition of PI as a constant, can be used in expressions as variable.
 */
public static final BigDecimal PI = new BigDecimal(
        "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679");

/**
 * Definition of e: "Euler's number" as a constant, can be used in expressions as variable.
 */
public static final BigDecimal e = new BigDecimal(
        "2.71828182845904523536028747135266249775724709369995957496696762772407663");
/**
 * What character to use for decimal separators.
 */
private static final char decimalSeparator = '.';
/**
 * What character to use for minus sign (negative values).
 */
private static final char minusSign = '-';
/**
 * The BigDecimal representation of the left parenthesis,
 * used for parsing varying numbers of function parameters.
 */
private static final LazyNumber PARAMS_START = new LazyNumber() {
    public BigDecimal eval() {
        return null;
    }

    @Override
    public String getString() {
        return null;
    }
};
/**
 * The {@link MathContext} to use for calculations.
 */
private MathContext mc = null;
/**
 * The original infix expression.
 */
private String expression = null;
/**
 * The cached RPN (Reverse Polish Notation) of the expression.
 */
private List<String> rpn = null;
/**
 * All defined operators with name and implementation.
 */
private Map<String, Operator> operators = new TreeMap<String, Operator>(String.CASE_INSENSITIVE_ORDER);
/**
 * All defined functions with name and implementation.
 */
private Map<String, LazyFunction> functions = new TreeMap<String, LazyFunction>(String.CASE_INSENSITIVE_ORDER);
/**
 * All defined variables with name and value.
 */
private Map<String, LazyNumber> variables = new TreeMap<String, LazyNumber>(String.CASE_INSENSITIVE_ORDER);

/**
 * Creates a new expression instance from an expression string with a given
 * default match context of {@link MathContext#DECIMAL32}.
 *
 * @param expression The expression. E.g. <code>"2.4*sin(3)/(2-4)"</code> or
 *                   <code>"sin(y)>0 & max(z, 3)>3"</code>
 */
public Expression(String expression) {
    this(expression, MathContext.DECIMAL32);
}


/**
 * Creates a new expression instance from an expression string with a given
 * default match context.
 *
 * @param expression         The expression. E.g. <code>"2.4*sin(3)/(2-4)"</code> or
 *                           <code>"sin(y)>0 & max(z, 3)>3"</code>
 * @param defaultMathContext The {@link MathContext} to use by default.
 */
public Expression(String expression, MathContext defaultMathContext) {
    this.mc = defaultMathContext;
    this.expression = expression;
    addOperator(new Operator("+", 20, true) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.add(v2, mc);
        }
    });
    addOperator(new Operator("-", 20, true) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.subtract(v2, mc);
        }
    });
    addOperator(new Operator("*", 30, true) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.multiply(v2, mc);
        }
    });
    addOperator(new Operator("/", 30, true) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.divide(v2, mc);
        }
    });
    addOperator(new Operator("%", 30, true) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.remainder(v2, mc);
        }
    });
    addOperator(new Operator("^", 40, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            /*-
             * Thanks to Gene Marin:
             * http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java
             */
            int signOf2 = v2.signum();
            double dn1 = v1.doubleValue();
            v2 = v2.multiply(new BigDecimal(signOf2)); // n2 is now positive
            BigDecimal remainderOf2 = v2.remainder(BigDecimal.ONE);
            BigDecimal n2IntPart = v2.subtract(remainderOf2);
            BigDecimal intPow = v1.pow(n2IntPart.intValueExact(), mc);
            BigDecimal doublePow = new BigDecimal(Math.pow(dn1,
                    remainderOf2.doubleValue()));

            BigDecimal result = intPow.multiply(doublePow, mc);
            if (signOf2 == -1) {
                result = BigDecimal.ONE.divide(result, mc.getPrecision(),
                        RoundingMode.HALF_UP);
            }
            return result;
        }
    });
    addOperator(new Operator("&&", 4, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            boolean b1 = !v1.equals(BigDecimal.ZERO);
            boolean b2 = !v2.equals(BigDecimal.ZERO);
            return b1 && b2 ? BigDecimal.ONE : BigDecimal.ZERO;
        }
    });

    addOperator(new Operator("||", 2, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            boolean b1 = !v1.equals(BigDecimal.ZERO);
            boolean b2 = !v2.equals(BigDecimal.ZERO);
            return b1 || b2 ? BigDecimal.ONE : BigDecimal.ZERO;
        }
    });

    addOperator(new Operator(">", 10, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.compareTo(v2) == 1 ? BigDecimal.ONE : BigDecimal.ZERO;
        }
    });

    addOperator(new Operator(">=", 10, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.compareTo(v2) >= 0 ? BigDecimal.ONE : BigDecimal.ZERO;
        }
    });

    addOperator(new Operator("<", 10, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.compareTo(v2) == -1 ? BigDecimal.ONE
                    : BigDecimal.ZERO;
        }
    });

    addOperator(new Operator("<=", 10, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.compareTo(v2) <= 0 ? BigDecimal.ONE : BigDecimal.ZERO;
        }
    });

    addOperator(new Operator("=", 7, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.compareTo(v2) == 0 ? BigDecimal.ONE : BigDecimal.ZERO;
        }
    });
    addOperator(new Operator("==", 7, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return operators.get("=").eval(v1, v2);
        }
    });

    addOperator(new Operator("!=", 7, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return v1.compareTo(v2) != 0 ? BigDecimal.ONE : BigDecimal.ZERO;
        }
    });
    addOperator(new Operator("<>", 7, false) {
        @Override
        public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
            return operators.get("!=").eval(v1, v2);
        }
    });

    addFunction(new Function("NOT", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            boolean zero = parameters.get(0).compareTo(BigDecimal.ZERO) == 0;
            return zero ? BigDecimal.ONE : BigDecimal.ZERO;
        }
    });

    addLazyFunction(new LazyFunction("IF", 3) {
        @Override
        public LazyNumber lazyEval(List<LazyNumber> lazyParams) {
            boolean isTrue = !lazyParams.get(0).eval().equals(BigDecimal.ZERO);
            return isTrue ? lazyParams.get(1) : lazyParams.get(2);
        }
    });

    addFunction(new Function("RANDOM", 0) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.random();
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("SIN", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.sin(Math.toRadians(parameters.get(0)
                    .doubleValue()));
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("COS", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.cos(Math.toRadians(parameters.get(0)
                    .doubleValue()));
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("TAN", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.tan(Math.toRadians(parameters.get(0)
                    .doubleValue()));
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("ASIN", 1) { // added by av
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.toDegrees(Math.asin(parameters.get(0)
                    .doubleValue()));
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("ACOS", 1) { // added by av
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.toDegrees(Math.acos(parameters.get(0)
                    .doubleValue()));
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("ATAN", 1) { // added by av
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.toDegrees(Math.atan(parameters.get(0)
                    .doubleValue()));
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("SINH", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.sinh(parameters.get(0).doubleValue());
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("COSH", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.cosh(parameters.get(0).doubleValue());
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("TANH", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.tanh(parameters.get(0).doubleValue());
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("RAD", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.toRadians(parameters.get(0).doubleValue());
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("DEG", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.toDegrees(parameters.get(0).doubleValue());
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("MAX", -1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            if (parameters.size() == 0) {
                throw new ExpressionException("MAX requires at least one parameter");
            }
            BigDecimal max = null;
            for (BigDecimal parameter : parameters) {
                if (max == null || parameter.compareTo(max) > 0) {
                    max = parameter;
                }
            }
            return max;
        }
    });
    addFunction(new Function("MIN", -1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            if (parameters.size() == 0) {
                throw new ExpressionException("MIN requires at least one parameter");
            }
            BigDecimal min = null;
            for (BigDecimal parameter : parameters) {
                if (min == null || parameter.compareTo(min) < 0) {
                    min = parameter;
                }
            }
            return min;
        }
    });
    addFunction(new Function("ABS", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            return parameters.get(0).abs(mc);
        }
    });
    addFunction(new Function("LOG", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.log(parameters.get(0).doubleValue());
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("LOG10", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            double d = Math.log10(parameters.get(0).doubleValue());
            return new BigDecimal(d, mc);
        }
    });
    addFunction(new Function("ROUND", 2) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            BigDecimal toRound = parameters.get(0);
            int precision = parameters.get(1).intValue();
            return toRound.setScale(precision, mc.getRoundingMode());
        }
    });
    addFunction(new Function("FLOOR", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            BigDecimal toRound = parameters.get(0);
            return toRound.setScale(0, RoundingMode.FLOOR);
        }
    });
    addFunction(new Function("CEILING", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            BigDecimal toRound = parameters.get(0);
            return toRound.setScale(0, RoundingMode.CEILING);
        }
    });
    addFunction(new Function("SQRT", 1) {
        @Override
        public BigDecimal eval(List<BigDecimal> parameters) {
            /*
             * From The Java Programmers Guide To numerical Computing
             * (Ronald Mak, 2003)
             */
            BigDecimal x = parameters.get(0);
            if (x.compareTo(BigDecimal.ZERO) == 0) {
                return new BigDecimal(0);
            }
            if (x.signum() < 0) {
                throw new ExpressionException(
                        "Argument to SQRT() function must not be negative");
            }
            BigInteger n = x.movePointRight(mc.getPrecision() << 1)
                    .toBigInteger();

            int bits = (n.bitLength() + 1) >> 1;
            BigInteger ix = n.shiftRight(bits);
            BigInteger ixPrev;

            do {
                ixPrev = ix;
                ix = ix.add(n.divide(ix)).shiftRight(1);
                // Give other threads a chance to work;
                Thread.yield();
            } while (ix.compareTo(ixPrev) != 0);

            return new BigDecimal(ix, mc.getPrecision());
        }
    });

    variables.put("e", new LazyNumber() {
        @Override
        public BigDecimal eval() {
            return e;
        }

        @Override
        public String getString() {
            return null;
        }
    });
    variables.put("PI", new LazyNumber() {
        @Override
        public BigDecimal eval() {
            return PI;
        }

        @Override
        public String getString() {
            return null;
        }
    });
    variables.put("TRUE", new LazyNumber() {
        @Override
        public BigDecimal eval() {
            return BigDecimal.ONE;
        }

        @Override
        public String getString() {
            return null;
        }
    });
    variables.put("FALSE", new LazyNumber() {
        @Override
        public BigDecimal eval() {
            return BigDecimal.ZERO;
        }

        @Override
        public String getString() {
            return null;
        }
    });

}

/**
 * Is the string a number?
 *
 * @param st The string.
 * @return <code>true</code>, if the input string is a number.
 */
private boolean isNumber(String st) {
    if (st.charAt(0) == minusSign && st.length() == 1) return false;
    if (st.charAt(0) == '+' && st.length() == 1) return false;
    if (st.charAt(0) == 'e' || st.charAt(0) == 'E') return false;
    for (char ch : st.toCharArray()) {
        if (!Character.isDigit(ch) && ch != minusSign
                && ch != decimalSeparator
                && ch != 'e' && ch != 'E' && ch != '+')
            return false;
    }
    return true;
}

/**
 * Implementation of the <i>Shunting Yard</i> algorithm to transform an
 * infix expression to a RPN expression.
 *
 * @param expression The input expression in infx.
 * @return A RPN representation of the expression, with each token as a list
 * member.
 */
private List<String> shuntingYard(String expression) {
    List<String> outputQueue = new ArrayList<String>();
    Stack<String> stack = new Stack<String>();

    Tokenizer tokenizer = new Tokenizer(expression);

    String lastFunction = null;
    String previousToken = null;
    while (tokenizer.hasNext()) {
        String token = tokenizer.next();
        if (isNumber(token)) {
            outputQueue.add(token);
        } else if (variables.containsKey(token)) {
            outputQueue.add(token);
        } else if (token.startsWith("'") && token.endsWith("'")) {
            outputQueue.add(token);
        } else if (functions.containsKey(token.toUpperCase(Locale.ROOT))) {
            stack.push(token);
            lastFunction = token;
        } else if (Character.isLetter(token.charAt(0))) {
            stack.push(token);
        } else if (",".equals(token)) {
            while (!stack.isEmpty() && !"(".equals(stack.peek())) {
                outputQueue.add(stack.pop());
            }
            if (stack.isEmpty()) {
                throw new ExpressionException("Parse error for function '"
                        + lastFunction + "'");
            }
        } else if (operators.containsKey(token)) {
            Operator o1 = operators.get(token);
            String token2 = stack.isEmpty() ? null : stack.peek();
            while (token2 != null &&
                    operators.containsKey(token2)
                    && ((o1.isLeftAssoc() && o1.getPrecedence() <= operators
                    .get(token2).getPrecedence()) || (o1
                    .getPrecedence() < operators.get(token2)
                    .getPrecedence()))) {
                outputQueue.add(stack.pop());
                token2 = stack.isEmpty() ? null : stack.peek();
            }
            stack.push(token);
        } else if ("(".equals(token)) {
            if (previousToken != null) {
                if (isNumber(previousToken)) {
                    throw new ExpressionException(
                            "Missing operator at character position "
                                    + tokenizer.getPos());
                }
                // if the ( is preceded by a valid function, then it
                // denotes the start of a parameter list
                if (functions.containsKey(previousToken.toUpperCase(Locale.ROOT))) {
                    outputQueue.add(token);
                }
            }
            stack.push(token);
        } else if (")".equals(token)) {
            while (!stack.isEmpty() && !"(".equals(stack.peek())) {
                outputQueue.add(stack.pop());
            }
            if (stack.isEmpty()) {
                throw new RuntimeException("Mismatched parentheses");
            }
            stack.pop();
            if (!stack.isEmpty()
                    && functions.containsKey(stack.peek().toUpperCase(
                    Locale.ROOT))) {
                outputQueue.add(stack.pop());
            }
        }
        previousToken = token;
    }
    while (!stack.isEmpty()) {
        String element = stack.pop();
        if ("(".equals(element) || ")".equals(element)) {
            throw new RuntimeException("Mismatched parentheses");
        }
        if (!operators.containsKey(element)) {
            throw new RuntimeException("Unknown operator or function: "
                    + element);
        }
        outputQueue.add(element);
    }
    return outputQueue;
}

/**
 * Evaluates the expression.
 *
 * @return The result of the expression.
 */
public BigDecimal eval() {

    Stack<LazyNumber> stack = new Stack<LazyNumber>();

    for (final String token : getRPN()) {
        if (operators.containsKey(token)) {
            final LazyNumber v1 = stack.pop();
            final LazyNumber v2 = stack.pop();
            LazyNumber number = new LazyNumber() {
                public BigDecimal eval() {
                    return operators.get(token).eval(v2.eval(), v1.eval());
                }

                @Override
                public String getString() {
                    return String.valueOf(operators.get(token).eval(v2.eval(), v1.eval()));
                }
            };
            stack.push(number);
        } else if (variables.containsKey(token)) {
            stack.push(new LazyNumber() {
                public BigDecimal eval() {
                    return variables.get(token).eval().round(mc);
                }

                @Override
                public String getString() {
                    return String.valueOf(variables.get(token).eval().round(mc));
                }
            });
        } else if (functions.containsKey(token.toUpperCase(Locale.ROOT))) {
            LazyFunction f = functions.get(token.toUpperCase(Locale.ROOT));
            ArrayList<LazyNumber> p = new ArrayList<LazyNumber>(
                    !f.numParamsVaries() ? f.getNumParams() : 0);
            // pop parameters off the stack until we hit the start of
            // this function's parameter list
            while (!stack.isEmpty() && stack.peek() != PARAMS_START) {
                p.add(0, stack.pop());
            }
            if (stack.peek() == PARAMS_START) {
                stack.pop();
            }
            if (!f.numParamsVaries() && p.size() != f.getNumParams()) {
                throw new ExpressionException("Function " + token + " expected " + f.getNumParams() + " parameters, got " + p.size());
            }
            LazyNumber fResult = f.lazyEval(p);
            stack.push(fResult);
        } else if ("(".equals(token)) {
            stack.push(PARAMS_START);
        } else {
            stack.push(new LazyNumber() {
                public BigDecimal eval() {
                    try {
                        return new BigDecimal(token, mc);
                    } catch (Exception e1) {
                        return new BigDecimal(0);
                    }
                }

                public String getString() {
                    return token;
                }
            });
        }
    }
    return stack.pop().eval().stripTrailingZeros();
}

/**
 * Sets the precision for expression evaluation.
 *
 * @param precision The new precision.
 * @return The expression, allows to chain methods.
 */
public Expression setPrecision(int precision) {
    this.mc = new MathContext(precision);
    return this;
}

/**
 * Sets the rounding mode for expression evaluation.
 *
 * @param roundingMode The new rounding mode.
 * @return The expression, allows to chain methods.
 */
public Expression setRoundingMode(RoundingMode roundingMode) {
    this.mc = new MathContext(mc.getPrecision(), roundingMode);
    return this;
}

/**
 * Adds an operator to the list of supported operators.
 *
 * @param operator The operator to add.
 * @return The previous operator with that name, or <code>null</code> if
 * there was none.
 */
public Operator addOperator(Operator operator) {
    return operators.put(operator.getOper(), operator);
}

/**
 * Adds a function to the list of supported functions
 *
 * @param function The function to add.
 * @return The previous operator with that name, or <code>null</code> if
 * there was none.
 */
public Function addFunction(Function function) {
    return (Function) functions.put(function.getName(), function);
}

/**
 * Adds a lazy function function to the list of supported functions
 *
 * @param function The function to add.
 * @return The previous operator with that name, or <code>null</code> if
 * there was none.
 */
public LazyFunction addLazyFunction(LazyFunction function) {
    return functions.put(function.getName(), function);
}

/**
 * Sets a variable value.
 *
 * @param variable The variable name.
 * @param value    The variable value.
 * @return The expression, allows to chain methods.
 */
public Expression setVariable(String variable, final BigDecimal value) {
    variables.put(variable, new LazyNumber() {
        @Override
        public BigDecimal eval() {
            return value;
        }

        @Override
        public String getString() {
            return "";
        }
    });
    return this;
}

/**
 * Sets a variable value.
 *
 * @param variable The variable to set.
 * @param value    The variable value.
 * @return The expression, allows to chain methods.
 */
public Expression setVariable(String variable, final String value) {
    if (isNumber(value)) {
        variables.put(variable, new LazyNumber() {
            @Override
            public BigDecimal eval() {
                return new BigDecimal(value);
            }

            @Override
            public String getString() {
                return value;
            }
        });
    } else if (value.startsWith("'") && value.endsWith("'")) {
        variables.put(variable, new LazyNumber() {
            @Override
            public BigDecimal eval() {
                try {
                    return new BigDecimal(0);
                } catch (Exception e1) {
                    return new BigDecimal(0);
                }
            }

            @Override
            public String getString() {
                return value;
            }
        });
    } else {
        expression = expression.replaceAll("(?i)\\b" + variable + "\\b", "("
                + value + ")");
        rpn = null;
    }

    return this;
}

/**
 * Sets a variable value.
 *
 * @param variable The variable to set.
 * @param value    The variable value.
 * @return The expression, allows to chain methods.
 */
public Expression with(String variable, BigDecimal value) {
    return setVariable(variable, value);
}

/**
 * Sets a variable value.
 *
 * @param variable The variable to set.
 * @param value    The variable value.
 * @return The expression, allows to chain methods.
 */
public Expression and(String variable, String value) {
    return setVariable(variable, value);
}

/**
 * Sets a variable value.
 *
 * @param variable The variable to set.
 * @param value    The variable value.
 * @return The expression, allows to chain methods.
 */
public Expression and(String variable, BigDecimal value) {
    return setVariable(variable, value);
}

/**
 * Sets a variable value.
 *
 * @param variable The variable to set.
 * @param value    The variable value.
 * @return The expression, allows to chain methods.
 */
public Expression with(String variable, String value) {
    return setVariable(variable, value);
}

/**
 * Get an iterator for this expression, allows iterating over an expression
 * token by token.
 *
 * @return A new iterator instance for this expression.
 */
public Iterator<String> getExpressionTokenizer() {
    return new Tokenizer(this.expression);
}

/**
 * Cached access to the RPN notation of this expression, ensures only one
 * calculation of the RPN per expression instance. If no cached instance
 * exists, a new one will be created and put to the cache.
 *
 * @return The cached RPN instance.
 */
private List<String> getRPN() {
    if (rpn == null) {
        rpn = shuntingYard(this.expression);
        validate(rpn);
    }
    return rpn;
}

/**
 * Check that the expression have enough numbers and variables to fit the
 * requirements of the operators and functions, also check
 * for only 1 result stored at the end of the evaluation.
 */
private void validate(List<String> rpn) {
    /*-
    * Thanks to Norman Ramsey:
    * http://http://stackoverflow.com/questions/789847/postfix-notation-validation
    */
    int counter = 0;
    Stack<Integer> params = new Stack<Integer>();
    for (String token : rpn) {
        if ("(".equals(token)) {
            // is this a nested function call?
            if (!params.isEmpty()) {
                // increment the current function's param count
                // (the return of the nested function call
                // will be a parameter for the current function)
                params.set(params.size() - 1, params.peek() + 1);
            }
            // start a new parameter count
            params.push(0);
        } else if (!params.isEmpty()) {
            if (functions.containsKey(token.toUpperCase(Locale.ROOT))) {
                // remove the parameters and the ( from the counter
                counter -= params.pop() + 1;
            } else {
                // increment the current function's param count
                params.set(params.size() - 1, params.peek() + 1);
            }
        } else if (operators.containsKey(token)) {
            //we only have binary operators
            counter -= 2;
        }
        if (counter < 0) {
            throw new ExpressionException("Too many operators or functions at: "
                    + token);
        }
        counter++;
    }
    if (counter > 1) {
        throw new ExpressionException("Too many numbers or variables");
    } else if (counter < 1) {
        throw new ExpressionException("Empty expression");
    }
}

/**
 * Get a string representation of the RPN (Reverse Polish Notation) for this
 * expression.
 *
 * @return A string with the RPN representation for this expression.
 */
public String toRPN() {
    StringBuilder result = new StringBuilder();
    for (String st : getRPN()) {
        if (result.length() != 0)
            result.append(" ");
        result.append(st);
    }
    return result.toString();
}

/**
 * Exposing declared variables in the expression.
 *
 * @return All declared variables.
 */
public Set<String> getDeclaredVariables() {
    return Collections.unmodifiableSet(variables.keySet());
}

/**
 * Exposing declared operators in the expression.
 *
 * @return All declared operators.
 */
public Set<String> getDeclaredOperators() {
    return Collections.unmodifiableSet(operators.keySet());
}

/**
 * Exposing declared functions.
 *
 * @return All declared functions.
 */
public Set<String> getDeclaredFunctions() {
    return Collections.unmodifiableSet(functions.keySet());
}

/**
 * LazyNumber interface created for lazily evaluated functions
 */
interface LazyNumber {
    BigDecimal eval();

    String getString();
}

/**
 * The expression evaluators exception class.
 */
public static class ExpressionException extends RuntimeException {
    private static final long serialVersionUID = 1118142866870779047L;

    public ExpressionException(String message) {
        super(message);
    }
}

public abstract class LazyFunction {
    /**
     * Name of this function.
     */
    private String name;
    /**
     * Number of parameters expected for this function.
     * <code>-1</code> denotes a variable number of parameters.
     */
    private int numParams;

    /**
     * Creates a new function with given name and parameter count.
     *
     * @param name      The name of the function.
     * @param numParams The number of parameters for this function.
     *                  <code>-1</code> denotes a variable number of parameters.
     */
    public LazyFunction(String name, int numParams) {
        this.name = name.toUpperCase(Locale.ROOT);
        this.numParams = numParams;
    }

    public String getName() {
        return name;
    }

    public int getNumParams() {
        return numParams;
    }

    public boolean numParamsVaries() {
        return numParams < 0;
    }

    public abstract LazyNumber lazyEval(List<LazyNumber> lazyParams);
}

/**
 * Abstract definition of a supported expression function. A function is
 * defined by a name, the number of parameters and the actual processing
 * implementation.
 */
public abstract class Function extends LazyFunction {

    public Function(String name, int numParams) {
        super(name, numParams);
    }

    public LazyNumber lazyEval(List<LazyNumber> lazyParams) {
        final List<BigDecimal> params = new ArrayList<BigDecimal>();
        for (LazyNumber lazyParam : lazyParams) {
            params.add(lazyParam.eval());
        }
        return new LazyNumber() {
            public BigDecimal eval() {
                return Function.this.eval(params);
            }

            @Override
            public String getString() {
                return String.valueOf(Function.this.eval(params));
            }
        };
    }

    /**
     * Implementation for this function.
     *
     * @param parameters Parameters will be passed by the expression evaluator as a
     *                   {@link List} of {@link BigDecimal} values.
     * @return The function must return a new {@link BigDecimal} value as a
     * computing result.
     */
    public abstract BigDecimal eval(List<BigDecimal> parameters);
}

/**
 * Abstract definition of a supported operator. An operator is defined by
 * its name (pattern), precedence and if it is left- or right associative.
 */
public abstract class Operator {
    /**
     * This operators name (pattern).
     */
    private String oper;
    /**
     * Operators precedence.
     */
    private int precedence;
    /**
     * Operator is left associative.
     */
    private boolean leftAssoc;

    /**
     * Creates a new operator.
     *
     * @param oper       The operator name (pattern).
     * @param precedence The operators precedence.
     * @param leftAssoc  <code>true</code> if the operator is left associative,
     *                   else <code>false</code>.
     */
    public Operator(String oper, int precedence, boolean leftAssoc) {
        this.oper = oper;
        this.precedence = precedence;
        this.leftAssoc = leftAssoc;
    }

    public String getOper() {
        return oper;
    }

    public int getPrecedence() {
        return precedence;
    }

    public boolean isLeftAssoc() {
        return leftAssoc;
    }

    /**
     * Implementation for this operator.
     *
     * @param v1 Operand 1.
     * @param v2 Operand 2.
     * @return The result of the operation.
     */
    public abstract BigDecimal eval(BigDecimal v1, BigDecimal v2);
}

/**
 * Expression tokenizer that allows to iterate over a {@link String}
 * expression token by token. Blank characters will be skipped.
 */
private class Tokenizer implements Iterator<String> {

    /**
     * Actual position in expression string.
     */
    private int pos = 0;

    /**
     * The original input expression.
     */
    private String input;
    /**
     * The previous token or <code>null</code> if none.
     */
    private String previousToken;

    /**
     * Creates a new tokenizer for an expression.
     *
     * @param input The expression string.
     */
    public Tokenizer(String input) {
        this.input = input.trim();
    }

    @Override
    public boolean hasNext() {
        return (pos < input.length());
    }

    /**
     * Peek at the next character, without advancing the iterator.
     *
     * @return The next character or character 0, if at end of string.
     */
    private char peekNextChar() {
        if (pos < (input.length() - 1)) {
            return input.charAt(pos + 1);
        } else {
            return 0;
        }
    }

    @Override
    public String next() {
        StringBuilder token = new StringBuilder();
        if (pos >= input.length()) {
            return previousToken = null;
        }
        char ch = input.charAt(pos);
        while (Character.isWhitespace(ch) && pos < input.length()) {
            ch = input.charAt(++pos);
        }
        if (Character.isDigit(ch)) {
            while ((Character.isDigit(ch) || ch == decimalSeparator
                    || ch == 'e' || ch == 'E'
                    || (ch == minusSign && token.length() > 0
                    && ('e' == token.charAt(token.length() - 1) || 'E' == token.charAt(token.length() - 1)))
                    || (ch == '+' && token.length() > 0
                    && ('e' == token.charAt(token.length() - 1) || 'E' == token.charAt(token.length() - 1)))
            ) && (pos < input.length())) {
                token.append(input.charAt(pos++));
                ch = pos == input.length() ? 0 : input.charAt(pos);
            }
        } else if (ch == minusSign
                && Character.isDigit(peekNextChar())
                && ("(".equals(previousToken) || ",".equals(previousToken)
                || previousToken == null || operators
                .containsKey(previousToken))) {
            token.append(minusSign);
            pos++;
            token.append(next());
        } else if (Character.isLetter(ch) || (ch == '_')) {
            while ((Character.isLetter(ch) || Character.isDigit(ch) || (ch == '_'))
                    && (pos < input.length())) {
                token.append(input.charAt(pos++));
                ch = pos == input.length() ? 0 : input.charAt(pos);
            }
        } else if (ch == '\'') {
            int nextPos = input.indexOf('\'', pos + 1);
            token.append(input.substring(pos, nextPos + 1));
            pos = nextPos + 1;
        } else if (ch == '(' || ch == ')' || ch == ',') {
            token.append(ch);
            pos++;
        } else {
            while (!Character.isLetter(ch) && !Character.isDigit(ch)
                    && ch != '_' && !Character.isWhitespace(ch)
                    && ch != '(' && ch != ')' && ch != ','
                    && (pos < input.length())) {
                token.append(input.charAt(pos));
                pos++;
                ch = pos == input.length() ? 0 : input.charAt(pos);
                if (ch == minusSign) {
                    break;
                }
            }
            if (!operators.containsKey(token.toString())) {
                throw new ExpressionException("Unknown operator '" + token
                        + "' at position " + (pos - token.length() + 1));
            }
        }
        return previousToken = token.toString();
    }

    @Override
    public void remove() {
        throw new ExpressionException("remove() not supported");
    }

    /**
     * Get the actual character position in the string.
     *
     * @return The actual character position.
     */
    public int getPos() {
        return pos;
    }

}

}

@uklimaschewski
Copy link
Collaborator

I am glad, this worked for you.
If you want to have your changes into the code here, you have to create a pull request with the changes.
If you do so, then also add proper unit tests and documentation (in code and readme.md).
Also, take care not to change the code formatting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants