Оценка математического выражения с переменными. (java 8)

Мне нужна дополнительная помощь в ответе на этот вопрос Оценка математического выражения, приведенного в строковой форме. Пользователь @Boann ответил на вопрос с помощью очень интересного алгоритма, который он также указывает, может быть изменен для принятия переменных. Мне удалось изменить его и заставить его работать, , но не знаю, как он разделяет компиляцию и оценку. Здесь мой код:

import java.util.HashMap;
import java.util.Map;

public class EvaluateExpressionWithVariabels {

@FunctionalInterface
interface Expression {
    double eval();
}

public static void main(String[] args){
    Map<String,Double> variables = new HashMap<>();     
    for (double x = 100; x <= +120; x++) {
        variables.put("x", x);
        System.out.println(x + " => " + eval("x+(sqrt(x))",variables).eval());
    }
}

public static Expression eval(final String str,Map<String,Double> variables) {
    return new Object() {
        int pos = -1, ch;

        //if check pos+1 is smaller than string length ch is char at new pos
        void nextChar() {
            ch = (++pos < str.length()) ? str.charAt(pos) : -1;
        }

        //skips 'spaces' and if current char is what was searched, if true move to next char return true
        //else return false
        boolean eat(int charToEat) {
            while (ch == ' ') nextChar();
            if (ch == charToEat) {
                nextChar();
                return true;
            }
            return false;
        }


        Expression parse() {
            nextChar();
            Expression x = parseExpression();
            if (pos < str.length()) throw new RuntimeException("Unexpected: " + (char)ch);
            return x;
        }

        // Grammar:
        // expression = term | expression `+` term | expression `-` term
        // term = factor | term `*` factor | term `/` factor
        // factor = `+` factor | `-` factor | `(` expression `)`
        //        | number | functionName factor | factor `^` factor

        Expression parseExpression() {
            Expression x = parseTerm();
            for (;;) {
                if (eat('+')) { // addition
                    Expression a = x, b = parseTerm();                      
                    x = (() -> a.eval() + b.eval());
                } else if (eat('-')) { // subtraction
                    Expression a = x, b = parseTerm();
                    x = (() -> a.eval() - b.eval());
                } else {
                    return x;
                }
            }
        }

        Expression parseTerm() {
            Expression x = parseFactor();
            for (;;) {
                if (eat('*')){
                     Expression a = x, b = parseFactor(); // multiplication
                     x = (() -> a.eval() * b.eval());
                }
                else if(eat('/')){
                     Expression a = x, b = parseFactor(); // division
                     x = (() -> a.eval() / b.eval());
                }
                else return x;
            }
        }

        Expression parseFactor() {
            if (eat('+')) return parseFactor(); // unary plus
            if (eat('-')){
                Expression b = parseFactor(); // unary minus
                return (() -> -1 * b.eval());
            }

            Expression x;
            int startPos = this.pos;
            if (eat('(')) { // parentheses
                x = parseExpression();
                eat(')');
            } else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers
                while ((ch >= '0' && ch <= '9') || ch == '.'){                     
                    nextChar();
                }
                double xx = Double.parseDouble(str.substring(startPos, this.pos));
                x = () -> xx;
            } else if (ch >= 'a' && ch <= 'z') { // functions
                while (ch >= 'a' && ch <= 'z') nextChar();
                String func = str.substring(startPos, this.pos);

                if ( variables.containsKey(func)){
                    x = () -> variables.get(func);
                }else{
                    double xx = parseFactor().eval();
                    if (func.equals("sqrt")) x = () -> Math.sqrt(xx);
                    else if (func.equals("sin")) x = () -> Math.sin(Math.toRadians(xx));
                    else if (func.equals("cos")) x = () -> Math.cos(Math.toRadians(xx));
                    else if (func.equals("tan")) x = () -> Math.tan(Math.toRadians(xx));                    
                    else throw new RuntimeException("Unknown function: " + func);
                }
            } else {
                throw new RuntimeException("Unexpected: " + (char)ch);
            }

            if (eat('^')){ 
                x = () -> {
                    double d =  parseFactor().eval();
                    return Math.pow(d,d); // exponentiation
                };
            }

            return  x;
        }
    }.parse();
}
}

Если вы посмотрите на его ответ, его главная

public static void main(String[] args) {
    Map<String,Double> variables = new HashMap<>();
    Expression exp = parse("x^2 - x + 2", variables);
    for (double x = -20; x <= +20; x++) {
        variables.put("x", x);
        System.out.println(x + " => " + exp.eval());
    }
}

Он вызывает функцию parse в этой строке Expression exp = parse("x^2 - x + 2", variables); для компиляции выражения один раз и использует значение for для его оценки несколько раз с уникальными значениями x. Что означает функция parse.

ps: Я прокомментировал вопрос пользователей без ответа.

Ответ 1

Извините за путаницу. Функция "parse", на которую я ссылаюсь, является просто существующей функцией eval, но переименована, так как она возвращает объект Expression.

Итак, у вас будет:

public static Expression parse(String str, Map<String,Double> variables) { ... }

И вызывать его:

Map<String,Double> variables = new HashMap<>();
Expression exp = parse("x+(sqrt(x))", variables);
for (double x = 100; x <= +120; x++) {
    variables.put("x", x);
    System.out.println(x + " => " + exp.eval());
}

Еще одна вещь: необходимо знать во время разбора, ссылается ли имя на переменную или функцию, чтобы узнать, принимает ли он аргумент, но вы не можете вызвать containsKey на карте переменных во время разбора, так как переменные могут отсутствовать на карте до тех пор, пока не будет вызываться exp.eval()! Одно из решений состоит в том, чтобы поместить функции в карту, поэтому вы можете вызвать containsKey на это:

    } else if (ch >= 'a' && ch <= 'z') { // functions and variables
        while (ch >= 'a' && ch <= 'z') nextChar();
        String name = str.substring(startPos, this.pos);
        if (functions.containsKey(name)) {
            DoubleUnaryOperator func = functions.get(name);
            Expression arg = parseFactor();
            x = () -> func.applyAsDouble(arg.eval());
        } else {
            x = () -> variables.get(name);
        }
    } else {

И затем где-нибудь на уровне класса инициализируйте карту functions:

private static final Map<String,DoubleUnaryOperator> functions = new HashMap<>();
static {
    functions.put("sqrt", x -> Math.sqrt(x));
    functions.put("sin", x -> Math.sin(Math.toRadians(x)));
    functions.put("cos", x -> Math.cos(Math.toRadians(x)));
    functions.put("tan", x -> Math.tan(Math.toRadians(x)));
}

(Также было бы хорошо определить карту functions как локальную переменную внутри синтаксического анализатора, но это добавляет немного больше накладных расходов во время каждого разбора.)

Ответ 2

Я думаю, что фактический код Expression, который содержит ссылку на карту, не был опубликован в ответ.

Для того, чтобы этот примерный код работал, выражение должно иметь некоторую память. На самом деле код манипулирует картой, и поскольку выражение содержит ссылку на него, каждый последующий вызов выражения eval -method примет заданное значение.

Итак, чтобы получить код, вам сначала понадобится реализация выражения, содержащая ссылку на карту. Но будьте осторожны с любыми побочными эффектами, которые могут иметь такое выражение.

Итак, для этого конкретного примера код должен быть чем-то вроде следующего (у вас нет времени, чтобы полностью посмотреть, будет ли он работать, но вы получите идею: важно то, что выражение содержит ссылку, которая изменяется снаружи):

static Expression parse(String expression, Map<String, String> variables) {
    return new PseudoCompiledExpression(expression, variables);
}
static class PseudoCompiledExpression implements Expression {
    Map<String, String> variables;
    Expression wrappedExpression;
    PseudoCompiledExpression(String expression, Map<String, String> variables) {
       this.variables = variables;
       wrappedExpression = eval(expression, variables);
    }
   public double eval() {
        // the state of the used map is altered from outside... 
        return wrappedException.eval();
   }

Ответ 3

Я нашел, что указанная выше грамматика не работает для возведения в степень. Это работает:

public class BcInterpreter {

static final String BC_SPLITTER = "[\\^\\(\\/\\*\\-\\+\\)]";
static Map<String,Double> variables = new HashMap<>();
private static final Map<String,DoubleUnaryOperator> functions = new HashMap<>();

static {
    functions.put("sqrt", x -> Math.sqrt(x));
    functions.put("sin", x -> Math.sin(Math.toRadians(x)));
    functions.put("cos", x -> Math.cos(Math.toRadians(x)));
    functions.put("tan", x -> Math.tan(Math.toRadians(x)));
    functions.put("round", x -> Math.round(x));
    functions.put("abs", x -> Math.abs(x));
    functions.put("ceil", x -> Math.ceil(x));
    functions.put("floor", x -> Math.floor(x));
    functions.put("log", x -> Math.log(x));
    functions.put("exp", x -> Math.exp(x));
    // TODO: add more unary functions here.
}

/**
 * Parse the expression into a lambda, and evaluate with the variables set from fields
 * in the current row.  The expression only needs to be evaluated one time.
 * @param recordMap
 * @param fd
 * @param script
 * @return
 */
static String materialize(Map<String, String> recordMap, FieldDesc fd, String script){
    // parse the expression one time and save the lambda in the field metadata
    if (fd.get("exp") == null) {
        fd.put("exp", parse(script, variables));
    }
    // set the variables to be used with the expression, once per row
    String[] tokens = script.split(BC_SPLITTER);
    for(String key : tokens) {
        if (key != null) {
            String val = recordMap.get(key.trim());
            if (val != null)
                variables.put(key.trim(), Double.parseDouble(val));
        }
    }
    // evaluate the expression with current row variables
    return String.valueOf(((Expression)(fd.get("exp"))).eval());
}

@FunctionalInterface
interface Expression {
    double eval();
}

static Map<String,Double> getVariables(){
    return variables;
}

public static Expression parse(final String str,Map<String,Double> variables) {
    return new Object() {
        int pos = -1, ch;

        //if check pos+1 is smaller than string length ch is char at new pos
        void nextChar() {
            ch = (++pos < str.length()) ? str.charAt(pos) : -1;
        }

        //skips 'spaces' and if current char is what was searched, if true move to next char return true
        //else return false
        boolean eat(int charToEat) {
            while (ch == ' ') nextChar();
            if (ch == charToEat) {
                nextChar();
                return true;
            }
            return false;
        }

        Expression parse() {
            nextChar();
            Expression x = parseExpression();
            if (pos < str.length()) throw new RuntimeException("Unexpected: " + (char)ch);
            return x;
        }

        // Grammar:
        // expression = term | expression `+` term | expression `-` term
        // term = factor | term `*` factor | term `/` factor
        // factor = base | base '^' base
        // base = '-' base | '+' base | number | identifier | function factor | '(' expression ')'

        Expression parseExpression() {
            Expression x = parseTerm();
            for (;;) {
                if (eat('+')) { // addition
                    Expression a = x, b = parseTerm();
                    x = (() -> a.eval() + b.eval());
                } else if (eat('-')) { // subtraction
                    Expression a = x, b = parseTerm();
                    x = (() -> a.eval() - b.eval());
                } else {
                    return x;
                }
            }
        }

        Expression parseTerm() {
            Expression x = parseFactor();
            for (;;) {
                if (eat('*')){
                    Expression a = x, b = parseFactor(); // multiplication
                    x = (() -> a.eval() * b.eval());
                } else if(eat('/')){
                    Expression a = x, b = parseFactor(); // division
                    x = (() -> a.eval() / b.eval());
                } else {
                    return x;
                }
            }
        }

        Expression parseFactor(){
            Expression x = parseBase();
            for(;;){
                if (eat('^')){
                    Expression a = x, b = parseBase();
                    x = (()->Math.pow(a.eval(),b.eval()));
                }else{
                    return x;
                }
            }
        }
        Expression parseBase(){
            int startPos = this.pos;
            Expression x;
            if (eat('-')){
                Expression b = parseBase();
                x = (()-> (-1)*b.eval());
                return x;
            }else if (eat('+')){
                x = parseBase();
                return x;
            }
            if (eat('(')) { // parentheses
                x = parseExpression();
                eat(')');
                return x;
            } else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers
                while ((ch >= '0' && ch <= '9') || ch == '.'){
                    nextChar();
                }
                double xx = Double.parseDouble(str.substring(startPos, this.pos));
                x = () -> xx;
                return x;
            } else if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') { // functions and variables
                while (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') nextChar();
                String name = str.substring(startPos, this.pos);
                if (functions.containsKey(name)) {
                    DoubleUnaryOperator func = functions.get(name);
                    Expression arg = parseFactor();
                    x = () -> func.applyAsDouble(arg.eval());
                } else {
                    x = () -> variables.get(name);
                }
                return x;
            }else {
                throw new RuntimeException("Unexpected: " + (char)ch);
            }
        }
    }.parse();
}
}