问题
I would like additional help on a answer to this question, Evaluating a math expression given in string form. The user @Boann answered the question with a very interesting algorithm that he also points out can be altered to accept variables. I've managed to alter it and get it to work, but dont know how he separates the compilation and evaluation. Here's my code:
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();
}
}
If you take a look at his answer his main
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());
}
}
He calls function parse
on this line Expression exp = parse("x^2 - x + 2", variables);
to compile the expression once and uses the for to evaluate it multiple times with a unique x values. What does the parse
function refer to.
ps: I have commented on the users question with no reply.
回答1:
Sorry for the confusion. The "parse
" function I referred to is simply the existing eval
function, but renamed since it returns an Expression
object.
So you'd have:
public static Expression parse(String str, Map<String,Double> variables) { ... }
And invoke it by:
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());
}
One other thing: It's necessary to know at parse time whether a name refers to a variable or a function, in order to know whether or not it takes an argument, but you can't call containsKey
on the variables map during the parse, since the variables might not be present in the map until exp.eval()
is called! One solution is to put functions in a map instead, so you can call containsKey
on that:
} 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 {
And then somewhere at class level, initialize the functions
map:
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)));
}
(It would also be okay to define the functions
map as a local variable inside the parser, but that adds a little bit more overhead during each parse.)
回答2:
I think the actual code of the Expression
which holds the map reference was not published on the answer.
In order for that sample code to work the expression must have some kind of memory. Actually the code manipulates the map and as the expression holds a reference to it, each subsequent call to the expression's eval
-method will take the adjusted value.
So in order to get the code working you will first need an expression implementation, which holds a map reference. But beware of any sideeffects, which such an expression may have.
So for that specific example, the code must be something like the following (don't have time to completely look whether it will work, but you will get the idea: the important thing is, that the expression holds a reference which is altered from outside):
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:
I found the above grammar to not work for exponentiation. This one works:
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's 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's 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();
}
}
来源:https://stackoverflow.com/questions/40975678/evaluating-a-math-expression-with-variables-java-8